Compare commits

...

47 Commits

Author SHA1 Message Date
ryan e93131ab46 [优化] 修复依赖 2026-06-04 12:03:00 +08:00
ryan 161e6c4e86 [优化] 更换 JWT 认证机制 2026-06-04 11:54:53 +08:00
ryan 3dbc7b3045 [优化] Header 认证 2026-06-04 11:18:20 +08:00
ryan 4a4189705a [优化] 禁用手动升级功能 2026-06-04 11:13:29 +08:00
ryan 6aa71a4da8 [优化] 更新认证机制,使用 OPENFLARE_TOKEN 替代 Bearer Token 2026-06-04 11:10:06 +08:00
ryan bdc96f6d8e [优化] 合并 2026-06-04 10:28:12 +08:00
ryan 1c1063f448 [优化] 修复错误 2026-06-04 10:21:07 +08:00
ryan 29fdc378a1 [优化] 代码优化 2026-06-04 10:12:53 +08:00
ryan bd659d493d [优化] 操作优化 2026-06-04 10:07:40 +08:00
ryan 6a2a6028c3 [优化] 日志优化 2026-06-04 10:00:37 +08:00
ryan 6e85f1158b [优化] 增加根目录选项 2026-06-04 09:56:03 +08:00
ryan e117e314d9 [优化] 代码优化Format 2026-06-04 09:31:27 +08:00
ryan fbb0909638 [优化] 上传部署包功能及相关界面优化 2026-06-04 09:13:29 +08:00
ryan 3eecd31868 [新增] 添加 API 反向代理功能支持 2026-06-03 22:48:04 +08:00
ryan 68d70bbbe8 [新增] 界面优化 2026-06-03 19:53:54 +08:00
ryan 08ec945e59 [新增] 界面优化 2026-06-03 19:52:38 +08:00
ryan 4401cb0d66 [新增] POW 与 WAF 规则合并 2026-06-03 19:09:01 +08:00
ryan 36ae6247f9 [新增] OpenFlare Pages 2026-06-03 17:48:01 +08:00
ryan 1088086399 [新增] OpenFlare Pages 2026-06-03 17:31:14 +08:00
ryan 2c74d042ed [新增] OpenFlare Pages 2026-06-03 17:09:02 +08:00
ryan 3ec607106d [新增] 对接 Uptime Kuma 2026-06-03 12:17:46 +08:00
ryan f671a96d8c [新增] 对接 Uptime Kuma 2026-06-03 11:58:00 +08:00
ryan fe5cf9021f [优化] 更新Dockerfile,优化构建过程 2026-06-02 23:49:17 +08:00
ryan 1be2461716 [优化] 修复构建 2026-06-02 23:43:10 +08:00
ryan a0e9484e37 [优化] 重构数据库迁移逻辑,整合遗留和Goose迁移处理 2026-06-02 23:35:02 +08:00
ryan 4ca6f2957b [优化] 重构数据库迁移逻辑,整合遗留和Goose迁移处理 2026-06-02 22:27:04 +08:00
ryan dfd040a9de [优化] 确保所有管理的进程在停止时被正确取消和清理 2026-06-02 21:30:48 +08:00
ryan f29292dd81 [优化] 添加进程管理功能,支持PID文件处理和孤儿进程清理 2026-06-02 21:08:04 +08:00
ryan 4566fc1f53 [优化] 增强 WebSocket 处理逻辑,添加上下文取消支持和关闭机制
[优化] 重构 WebSocket 处理逻辑,添加消息处理接口和心跳机制
2026-06-02 19:37:03 +08:00
ryan 4e58bdd85b [优化] 重构 WebSocket 客户端,整合共享连接逻辑并简化代码 2026-06-02 17:35:32 +08:00
ryan c009b9e283 [优化] 修复增强 2026-06-02 17:24:12 +08:00
ryan 4e33e0e521 [优化] 添加进程重启机制和指数退避策略以增强稳定性 2026-06-02 17:18:14 +08:00
ryan 7252fb6285 [优化] 重构进程管理逻辑,添加自动重启和退避机制 2026-06-02 17:12:34 +08:00
ryan 2220e45989 [优化] 更新 Docker 部署命令,增加对 HTTP3的支持 2026-06-02 16:49:15 +08:00
ryan 6158a487cf [优化] 添加多语言支持的验证页面文本 2026-06-02 16:11:46 +08:00
ryan 9f9c609809 [优化] 添加 HTTP/3 支持配置选项 2026-06-02 16:07:44 +08:00
ryan e4c6ce9062 [优化] 添加自定义状态码匹配方法 2026-06-02 08:34:03 +08:00
ryan 81dd44c8fc [优化] 添加自定义状态码匹配方法 2026-06-02 08:31:39 +08:00
ryan 3825a7f29a [优化] 自定义不存在页面返回状态码 2026-06-02 08:14:15 +08:00
ryan d21643feed [优化] 更新文档 2026-06-02 00:15:51 +08:00
ryan 2525664013 [优化] 更新文档 2026-06-02 00:13:39 +08:00
ryan a850b0a188 [优化] 修复重启 frpc 挂掉问题 2026-06-01 22:45:21 +08:00
ryan fd8148c0db [优化] 界面优化 2026-06-01 22:33:41 +08:00
ryan edd98f4ff0 [优化] 优化 2026-06-01 22:24:41 +08:00
ryan d8f98e218f [优化] 修复一个升级数据库的错误 2026-06-01 22:00:37 +08:00
ryan fefe205158 [优化] 增加 FRPS WebUI 支持,添加相关配置和数据库迁移 2026-06-01 21:59:42 +08:00
ryan d6e7e2baa2 [优化] 增加自动更新功能,支持更新请求和版本管理 2026-06-01 21:50:22 +08:00
241 changed files with 24118 additions and 6810 deletions
+8
View File
@@ -1,5 +1,6 @@
.git
.idea
.github
anubis-source
**/node_modules
**/.next
@@ -9,3 +10,10 @@ anubis-source
**/coverage
**/*.db
**/*.log
tmp
logs
.DS_Store
.env
.env.*
docker-compose*.yml
+331 -205
View File
@@ -1,7 +1,7 @@
name: Release
permissions:
contents: write
name: Release
permissions:
contents: write
on:
workflow_dispatch:
inputs:
@@ -11,20 +11,20 @@ on:
type: string
push:
tags: ["v*"]
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.version.outputs.should_run }}
version: ${{ steps.version.outputs.version }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.version.outputs.should_run }}
version: ${{ steps.version.outputs.version }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve version metadata
id: version
env:
@@ -52,194 +52,320 @@ jobs:
fi
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [[ "$VERSION" =~ ^v[0-9]+(\.[0-9]+)*$ ]]; then
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
fi
build-frontend:
needs: prepare
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Build Frontend
env:
CI: ""
VERSION: ${{ needs.prepare.outputs.version }}
run: |
cd openflare_server/web
corepack enable
pnpm install --frozen-lockfile
NEXT_PUBLIC_APP_VERSION="$VERSION" pnpm build
- name: Upload Frontend Artifact
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: openflare_server/web/build
retention-days: 1
build-binaries:
needs:
- prepare
- build-frontend
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-server-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-server-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-server-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-server-darwin-arm64
- goos: windows
goarch: amd64
asset_name: openflare-server-windows-amd64.exe
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download Frontend Artifact
uses: actions/download-artifact@v4
with:
name: frontend-build
path: openflare_server/web/build
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: openflare_server/go.mod
- name: Build Server
working-directory: openflare_server
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p ../dist
go build -trimpath -ldflags "-s -w -X 'openflare/common.Version=$VERSION'" -o "../dist/$ASSET_NAME" .
- name: Upload Binary Artifact
uses: actions/upload-artifact@v4
with:
name: server-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/${{ matrix.asset_name }}
retention-days: 1
build-agent-binaries:
needs: prepare
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-agent-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-agent-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-agent-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-agent-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: openflare_agent/go.mod
- name: Build Agent
working-directory: openflare_agent
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p ../dist
go build -trimpath -ldflags "-s -w -X 'openflare-agent/internal/config.Version=$VERSION'" -o "../dist/$ASSET_NAME" ./cmd/agent
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [[ "$VERSION" =~ ^v[0-9]+(\.[0-9]+)*$ ]]; then
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
fi
build-frontend:
needs: prepare
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Build Frontend
env:
CI: ""
VERSION: ${{ needs.prepare.outputs.version }}
run: |
cd openflare_server/web
corepack enable
pnpm install --frozen-lockfile
NEXT_PUBLIC_APP_VERSION="$VERSION" pnpm build
- name: Upload Frontend Artifact
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: openflare_server/web/build
retention-days: 1
build-binaries:
needs:
- prepare
- build-frontend
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-server-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-server-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-server-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-server-darwin-arm64
- goos: windows
goarch: amd64
asset_name: openflare-server-windows-amd64.exe
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download Frontend Artifact
uses: actions/download-artifact@v4
with:
name: frontend-build
path: openflare_server/web/build
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: openflare_server/go.mod
- name: Build Server
working-directory: openflare_server
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p ../dist
go build -trimpath -ldflags "-s -w -X 'openflare/common.Version=$VERSION'" -o "../dist/$ASSET_NAME" .
- name: Upload Binary Artifact
uses: actions/upload-artifact@v4
with:
name: server-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/${{ matrix.asset_name }}
retention-days: 1
build-agent-binaries:
needs: prepare
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-agent-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-agent-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-agent-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-agent-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: openflare_agent/go.mod
- name: Build Agent
working-directory: openflare_agent
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p ../dist
go build -trimpath -ldflags "-s -w -X 'openflare-agent/internal/config.Version=$VERSION'" -o "../dist/$ASSET_NAME" ./cmd/agent
(cd ../dist && sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256")
- name: Upload Agent Artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{ matrix.goos }}-${{ matrix.goarch }}
- name: Upload Agent Artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
dist/${{ matrix.asset_name }}
dist/${{ matrix.asset_name }}.sha256
retention-days: 1
release:
needs:
- prepare
- build-binaries
- build-agent-binaries
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
steps:
- name: Download Server Artifacts
uses: actions/download-artifact@v4
with:
pattern: "server-*"
path: dist
merge-multiple: true
- name: Download Agent Artifacts
uses: actions/download-artifact@v4
with:
pattern: "agent-*"
path: dist
merge-multiple: true
- name: Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.prepare.outputs.version }}
name: ${{ needs.prepare.outputs.version }}
target_commitish: ${{ github.sha }}
files: dist/*
draft: false
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
retention-days: 1
build-relay-binaries:
needs: prepare
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-relay-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-relay-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-relay-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-relay-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: openflare_relay/go.mod
- name: Build Relay
working-directory: openflare_relay
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p ../dist
go build -trimpath -ldflags "-s -w -X 'openflare-relay/internal/config.Version=$VERSION'" -o "../dist/$ASSET_NAME" ./cmd/relay
(cd ../dist && sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256")
- name: Upload Relay Artifact
uses: actions/upload-artifact@v4
with:
name: relay-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
dist/${{ matrix.asset_name }}
dist/${{ matrix.asset_name }}.sha256
retention-days: 1
build-flared-binaries:
needs: prepare
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflared-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflared-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflared-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflared-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: openflared/go.mod
- name: Build Flared
working-directory: openflared
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p ../dist
go build -trimpath -ldflags "-s -w -X 'openflare-flared/internal/config.Version=$VERSION'" -o "../dist/$ASSET_NAME" ./cmd/flared
(cd ../dist && sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256")
- name: Upload Flared Artifact
uses: actions/upload-artifact@v4
with:
name: flared-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
dist/${{ matrix.asset_name }}
dist/${{ matrix.asset_name }}.sha256
retention-days: 1
release:
needs:
- prepare
- build-binaries
- build-agent-binaries
- build-relay-binaries
- build-flared-binaries
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
steps:
- name: Download Server Artifacts
uses: actions/download-artifact@v4
with:
pattern: "server-*"
path: dist
merge-multiple: true
- name: Download Agent Artifacts
uses: actions/download-artifact@v4
with:
pattern: "agent-*"
path: dist
merge-multiple: true
- name: Download Relay Artifacts
uses: actions/download-artifact@v4
with:
pattern: "relay-*"
path: dist
merge-multiple: true
- name: Download Flared Artifacts
uses: actions/download-artifact@v4
with:
pattern: "flared-*"
path: dist
merge-multiple: true
- name: Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.prepare.outputs.version }}
name: ${{ needs.prepare.outputs.version }}
target_commitish: ${{ github.sha }}
files: dist/*
draft: false
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1
View File
@@ -50,3 +50,4 @@ go.work.sum
*-source
*-source.*
.codex*
+24 -41
View File
@@ -2,23 +2,11 @@
本文件是 OpenFlare 的 AI 接手入口,不承载详细设计、规范和计划。接手项目时,请根据以下分层文档指引进行阅读与开发:
## 1. 核心必读文档(Level 3 & Level 4)- 必须阅读 ⚠️
### 面向 AI 的开发指导规范 (AI Guidelines) 必须阅读
为了理解 OpenFlare 的设计理念、产品边界、核心机制以及代码编写的工程约束,**AI 在接手项目时必须首先且完整阅读以下文档**:
### Level 3: 面向贡献者的参阅文档 (Contributor References)
* **[docs/design/index.md](./docs/design/index.md)**
*作用:理解当前 MVP 的产品范围、系统边界、核心对象和长期约束。*
* **[docs/design/architecture.md](./docs/design/architecture.md)**
*作用:理解 Server、Agent、OpenResty 与前端的职责边界与网络拓扑。*
* **[docs/design/release-model.md](./docs/design/release-model.md)**
*作用:理解配置发布、激活、回滚与 Agent 节点配置应用的模型。*
* **[docs/design/development.md](./docs/design/development.md)**
*作用:了解如何搭建本地开发环境,运行后端 Server、Agent 和前端开发服务器,以及运行测试与构建的命令。*
* **[docs/design/repository.md](./docs/design/repository.md)**
*作用:熟悉仓库的整体物理结构和各子目录的职责。*
### Level 4: 面向 AI 的开发指导规范 (AI Guidelines)
* **[docs/guildline/development-constraints.md](./docs/guildline/development-constraints.md)**
*作用:掌握核心后端/Agent/前端分层约束、数据模型规范、数据库迁移升级协议、API 与鉴权设计准则。*
* **[docs/guildline/Guidelines.md](./docs/guildline/Guidelines.md)**
@@ -26,43 +14,38 @@
* **[docs/guildline/Project.md](./docs/guildline/Project.md)**
*作用:针对 OpenFlare 后端特定的控制器参数解析、响应处理、纯净工具类与数据库逻辑完全隔离、Go 泛型切片去重及 JSON 序列化避坑细则。*
---
## 2. 按需查阅文档(Level 2)- 根据需求阅读 💡
当开发任务涉及具体的系统部署、升级、接口联调或配置字段查阅时,**AI 应当根据需求阅读相应的参考手册**:
### Level 2: 面对高级用户/开发者的参阅文档 (Reference Manuals)
### 系统参阅文档 按需查阅
* **[docs/reference/configuration.md](./docs/reference/configuration.md)**
*作用:系统启动时支持的所有环境变量、命令行参数、运行时 Option 选项和 Agent 配置文件字段。*
* **[docs/reference/cli.md](./docs/reference/cli.md)**
*作用:Server 与 Agent 可用的命令行参数、安装/卸载脚本参数等参考。*
* **[docs/reference/api.md](./docs/reference/api.md)**
*作用:管理端 API 与 Agent API 的响应结构、路径和详细鉴权约定。*
* **[docs/reference/deployment.md](./docs/reference/deployment.md)**
### 面向开发者的文档 按需查阅
* **[docs/design/index.md](./docs/design/index.md)**
*作用:理解当前 MVP 的产品范围、系统边界、核心对象和长期约束。*
* **[docs/design/architecture.md](./docs/design/architecture.md)**
*作用:理解 Server、Agent、OpenResty 与前端的职责边界与网络拓扑。*
* **[docs/design/agent-design.md](./docs/design/agent-design.md)**
*作用:理解 Agent 设计原则、与 Server 交互时序、OpenResty 管控、配置版本发布与三阶段异常回滚模型。*
* **[docs/design/development.md](./docs/design/development.md)**
*作用:了解如何搭建本地开发环境,运行后端 Server、Agent 和前端开发服务器,以及运行测试与构建的命令。*
* **[docs/design/repository.md](./docs/design/repository.md)**
*作用:熟悉仓库的整体物理结构和各子目录的职责。*
### 部署与升级指南
* **[docs/deployment/deployment.md](./docs/deployment/deployment.md)**
*作用:理解 Server 和 Agent 的单机、Docker 部署配置,以及 Agent 接入、升级、卸载和联调步骤。*
* **[docs/reference/server.md](./docs/reference/server.md)**
* **[docs/deployment/server.md](./docs/deployment/server.md)**
*作用:如何配置系统配置、服务环境变量并正确启动 Server 服务。*
* **[docs/reference/agent.md](./docs/reference/agent.md)**
* **[docs/deployment/agent.md](./docs/deployment/agent.md)**
*作用:理解 Agent 接入的 discovery/agent 令牌鉴权机制、本地配置文件及 Docker 部署参数。*
* **[docs/reference/upgrade.md](./docs/reference/upgrade.md)**
* **[docs/deployment/upgrade.md](./docs/deployment/upgrade.md)**
*作用:Server 及各代理节点 Agent 的升级步骤与维护策略。*
---
## 3. 新手与业务教程(Level 1)- 体验与排障参考 📘
如果任务涉及优化最终用户体验、丰富业务能力或排查常见故障,可参阅面向普通用户的指南:
### Level 1: 面向新手用户的教程文档 (Novice Tutorials)
* **[docs/guide/quick-start.md](./docs/guide/quick-start.md)**:五分钟内基于 Docker Compose 快速跑起 Server 和首个 Agent 节点的完整闭环。
* **[docs/guide/usage.md](./docs/guide/usage.md)**:反向代理网站、源站、证书托管、配置发布与回滚的常规界面操作与观测功能使用指南。
* **[docs/guide/sso.md](./docs/guide/sso.md)**:系统如何配置 GitHub OAuth 及标准 OIDC 第三方登录,以及绑定本地账户的流程。
* **[docs/guide/first-site.md](./docs/guide/first-site.md)**:从零开始配置、发布并验证第一个代理网站的完整步骤。
* **[docs/guide/troubleshooting.md](./docs/guide/troubleshooting.md)**:常见数据库迁移、节点离线、OpenResty 校验失败、SSL 证书失效等故障的表现症状及标准排障路径。
---
## 执行要求
* 如果实现内容超出 [产品边界](./docs/design/index.md),先修改设计文档,再继续编码。
@@ -77,11 +60,11 @@
* 产品范围或系统边界变化:更新 `docs/design/index.md`
* 系统结构、模块职责变化:更新 `docs/design/architecture.md`
* 发布、同步、回滚模型变化:更新 `docs/design/release-model.md`
* 发布、同步、回滚与 Agent 模型变化:更新 `docs/design/agent-design.md`
* 业务分层、数据模型边界、接口约定、阶段原则、测试基线变化:更新 `docs/guildline/development-constraints.md`
* 后端开发规范、代码质量要求、重构模式、去重逻辑与避坑指南变化:更新 `docs/guildline/` 下的对应开发准则文件
* 产品启动、部署、升级、联调方式变化:更新 `docs/guide/quick-start.md`、`docs/reference/deployment.md` 和 `README.md`
* 产品启动、部署、升级、联调方式变化:更新 `docs/guide/quick-start.md`、`docs/deployment/deployment.md` 和 `README.md`
* 用户操作路径、常见场景变化:更新 `docs/guide/usage.md`
* 本地开发、测试、构建方式变化:更新 `docs/design/development.md`
* 常见故障、排查路径变化:更新 `docs/guide/troubleshooting.md`
* 环境变量、命令行参数、运行时配置、Agent 配置变化:更新 `docs/reference/configuration.md`
* **任何代码、配置或文档变更完成后:必须在 [`docs/changelog/index.md`](./docs/changelog/index.md) 的 `[Unreleased]` 区块补充对应条目(新增 / 变更 / 修复),格式遵循文件内已有模板。**
+30 -26
View File
@@ -4,7 +4,7 @@
**[📖 English](./README.md) | [中文](./README.zh-CN.md)**
A lightweight, self-hosted control plane for OpenResty that manages reverse proxy rules, configuration releases, node synchronization, TLS certificates, and observability.
OpenFlare is an open-source CDN orchestration and edge security platform. It supports reverse proxies, centralized configuration synchronization, secure intranet penetration (Tunnels), dynamic WAF protection, and anti-CC challenges.
</div>
@@ -21,9 +21,9 @@ A lightweight, self-hosted control plane for OpenResty that manages reverse prox
</p>
> [!WARNING]
> After the first login with the `root` user, you **must** change the default password `123456`.
>
> This BETA version is a temporary product in the development and testing phase. It may contain unknown issues and should not be used in production environments.
> After logging in for the first time with the `root` user, make sure to change the default password `123456`.
>
> The BETA version is a temporary product for the development and testing phase. It may contain unknown issues and should not be used in production environments.
## Documentation
@@ -31,20 +31,22 @@ A lightweight, self-hosted control plane for OpenResty that manages reverse prox
Quick links:
* [Quick Start](https://open-flare.pages.dev/guide/quick-start)
* [Deployment Guide](https://open-flare.pages.dev/reference/deployment)
* [Quick Start](https://open-flare.pages.dev/en/guide/quick-start)
* [Deployment Guide](https://open-flare.pages.dev/en/guide/deployment)
* [Configuration Reference](https://open-flare.pages.dev/reference/configuration)
* [System Design](https://open-flare.pages.dev/design/)
## Core Features
* **Reverse Proxy Configuration**: Website management and multi-domain binding
* **Configuration Lifecycle**: Preview, release, activation, and historical rollback
* **Agent Management**: Auto-registration, heartbeat, sync, validation, reload, and failure rollback
* **OpenResty Administration**: Main configuration, performance tuning, caching, and Lua resource hosting
* **WAF Protection**: Global and custom rule groups with IP/CIDR and geographic blacklist/whitelist
* **Certificate Management**: TLS certificates, domain assets, node credentials, and version control
* **Observability**: Request aggregation, access analytics, resource snapshots, health events, and node metrics
* **Centralized Real-Time Config Sync**: Sync configurations across all nodes in real time via WebSockets and heartbeats with sub-second hot reload. Instantly retrieve alerts and statuses. No manual SSH login or online patching required.
* **Distributed CDN Orchestration**: Orchestrate scattered and independent OpenResty nodes into a highly collaborative distributed Content Delivery Network (CDN) fleet, supporting website-level multi-domain aggregation, upstream Keepalive, and multi-origin load balancing.
* **Secure Intranet Penetration (Tunnels)**: An open-source alternative to Cloudflare Tunnels. Expose local intranet services securely to the public network without a public IP or exposing inbound ports.
* **Edge WAF Safety Protection**: Provides global and custom rule groups, supporting IP/CIDR filtering, MaxMind GeoIP country-level regional access control, asynchronous differential synchronization of IP group members without Nginx reloads, and custom block responses.
* **Anti-CC & Human-Machine Challenge (PoW)**: Built-in high-performance client-side cryptographic Proof of Work challenges (similar to Turnstile) to block and intercept botnets and scrapers at the gateway edge in seconds.
* **Publish & Sync Model**: Based on immutable configuration versions (`YYYYMMDD-NNN`), preview and compare differences before publishing, a single globally active version, and one-click sub-second rollbacks.
* **Three-Stage Disaster Recovery & Rollback**: Supports automatic node backup rollback, a built-in safety fallback page (Port 80/503 keeping status monitoring and security interception active), and an abnormal configuration blocklist.
* **Automated Certificate Hosting**: Supports dynamic certificate upload, automatic multi-domain certificate matching and binding, ACME automatic renewal, and full lifecycle status tracking.
* **Unified Observability**: Aggregates node request counts, provides real-time access analysis, host/Nginx resource snapshots, health logs, and a re-upload buffer for network fluctuations.
## Quick Start
@@ -98,12 +100,14 @@ Default credentials:
### 2. Install Agent
Before installing an Agent, install OpenResty on the target node, or use the Docker image with OpenResty built-in.
Before installing an Agent, please install OpenResty on the target node first, or use the Agent Docker image with OpenResty built-in.
You can copy the installation command from the Dashboard → Node Management → Details → Node Info, or use the script below:
You can copy the installation command from **Node Management -> Details -> Node Info -> Node Token & Deployment** in the control panel, or directly use the scripts below:
#### Docker Deployment
For Docker deployment, you can directly run the Agent image:
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
@@ -116,7 +120,7 @@ docker run -d --name openflare-agent --restart unless-stopped \
#### Local Installation
Using `discovery_token`:
Using `discovery_token` to register:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
@@ -132,26 +136,26 @@ curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/inst
--agent-token YOUR_AGENT_TOKEN
```
The installation script defaults to `/opt/openflare-agent`, creates a `openflare-agent.service`, auto-detects `openresty`, and supports re-execution for upgrades.
The installation script defaults to `/opt/openflare-agent`, creates a `openflare-agent.service`, automatically searches for `openresty`, and can be executed repeatedly to reinstall or upgrade the Agent.
### 3. Uninstall Agent
To completely uninstall the Agent and clean local data:
To completely uninstall the Agent and clear local data, run:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/uninstall-agent.sh | bash
```
The uninstall script stops and removes the `openflare-agent.service`, deletes the `/opt/openflare-agent` directory, and does not remove OpenResty.
The uninstallation script will stop and remove the `openflare-agent.service`, and delete the entire `/opt/openflare-agent` directory. It will not delete the local OpenResty installation.
### 4. Deploy Your First Configuration
### 4. Publish Your First Configuration
1. Log in to the dashboard and create a reverse proxy rule
2. Preview changes or view the changelog before publishing
3. Activate the new version
4. Agents receive notifications via WebSocket or pull configuration on next heartbeat
1. Log in to the management panel and add a reverse proxy rule.
2. View the preview or change summary before publishing.
3. Activate the new version.
4. Agents will receive the configuration and apply it via WebSocket notification or subsequent heartbeats.
Versions are immutable with format `YYYYMMDD-NNN`. Rollback is performed by reactivating a previous version.
The version number format is fixed as `YYYYMMDD-NNN`. Historical versions are immutable, and rollback is achieved by reactivating an older version.
## UI Preview
@@ -174,7 +178,7 @@ The management panel includes:
* Reverse Proxy Rules
* Configuration Versions
* Node Management
* Application History
* Application Records
* TLS Certificates
* Domain Management
* WAF Rule Groups
+14 -10
View File
@@ -2,11 +2,13 @@
# OpenFlare
轻量、自托管的 OpenResty 控制面,用于管理反向代理规则、配置发布、节点同步、TLS 证书与基础可观测能力。
**[English](./README.md) | [📖 中文](./README.zh-CN.md)**
OpenFlare 是开源 CDN 编排与边缘安全平台。它支持反向代理、集中式配置同步、内网穿透(Tunnels)、动态 WAF 防护以及防 CC 挑战。
</div>
<p align="center
<p align="center">
<a href="https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/LICENSE">
<img src="https://img.shields.io/github/license/Rain-kl/OpenFlare?color=brightgreen" alt="license">
</a>
@@ -36,13 +38,15 @@
## 核心能力
* 反向代理网站配置与多域名绑定
* 配置预览、发布、激活与历史回滚
* Agent 自动注册、心跳、同步、校验、reload 与失败回滚
* OpenResty 主配置、性能参数、缓存参数与 Lua 资源托管
* WAF 全局/自定义规则组,支持 IP/IP 段与国家级地域黑白名单
* TLS 证书、域名资产、节点凭证与版本状态管理
* 请求聚合、访问分析、资源快照、健康事件与节点详情
* **中心化实时配置同步**:通过 WebSocket 与心跳实现全网节点配置秒级同步下发与热生效,告警与状态即时回收,无须手动 SSH 登录或在线打补丁。
* **分布式 CDN 编排集群**:将分散独立的 OpenResty 节点编排为高度协同的分布式内容分发网络(CDN)舰队,支持网站级多域名聚合、上游 Keepalive 与多源站负载均衡。
* **安全内网穿透(Tunnels)**:开源版的 Cloudflare Tunnels。无须公网 IP 或暴露入向端口,安全反向穿透本地内网服务至公网。
* **边缘 WAF 安全防护**:提供全局及自定义规则组,支持 IP/CIDR 过滤、MaxMind GeoIP 国家级地域准入、IP 组成员异步差分同步免 Nginx 重载以及自定义拦截响应。
* **防 CC 与人机挑战(PoW)**:内置高性能客户端密码学 Proof of Work 挑战(类似 Turnstile),在网关边缘秒级拦截并阻断僵尸网络与爬虫。
* **发布与同步模型**:基于不可变配置版本(`YYYYMMDD-NNN`)、发布前预览对比差异、全局单激活版本与一键秒级回滚。
* **三阶段容灾回滚**:支持节点备份自动回滚、内置安全兜底页面(Port 80/503 保持状态监控与安全拦截)及异常配置阻断名单。
* **证书托管自动化**:支持证书动态上传、多域名证书自动匹配绑定、ACME 自动续期与全生命周期状态追踪。
* **统一观测与可观测性**:聚合节点请求数、实时访问分析、宿主机与 Nginx 资源快照、健康日志及网络波动补传缓冲。
## 快速开始
@@ -108,7 +112,7 @@ Docker 部署可直接运行 Agent 镜像:
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-p 80:80 -p 443:443/tcp -p 443:443/udp \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
+1
View File
@@ -7,6 +7,7 @@ export default defineConfig({
title: 'OpenFlare',
lastUpdated: true,
cleanUrls: true,
ignoreDeadLinks: true,
metaChunk: true,
srcExclude: [
'zh/**',
+411
View File
@@ -0,0 +1,411 @@
---
sidebar: false
---
# 更新日志
本文件记录 OpenFlare 每个版本的重要变更。
格式基于 [Keep a Changelog](http://keepachangelog.com/),版本号遵循 [语义化版本](http://semver.org/)。
## 重大变更
> [!IMPORTANT]
> 2.3.2 开始使用 JWT_SECRET 环境变量替代 SESSION_SECRET 进行管理端 API 的 JWT 签名密钥管理。SESSION_SECRET 将会在之后的版本中逐步废弃,请务必尽快迁移到 JWT_SECRET。
## [Unreleased]
### 说明
### 新增
### 变更
---
## [v2.3.2] - 2026-06-04
### 说明
> [!IMPORTANT]
> 2.3.2 开始使用 JWT_SECRET 环境变量替代 SESSION_SECRET 进行管理端 API 的 JWT 签名密钥管理。SESSION_SECRET 将会在之后的版本中逐步废弃,请务必尽快迁移到 JWT_SECRET。
### 新增
- 新增 `JWT_SECRET` 环境变量,专用于管理端 API JWT 签名密钥;生产环境必须显式配置
- 新增 VitePress 更新日志页面(`docs/changelog/index.md`),记录所有版本变更历史
### 变更
- 管理端 API 鉴权框架迁移至 `gin-jwt`
- 认证方式变更为 Headers 认证.
- `JWT_SECRET` 优先于 `SESSION_SECRET` 用于 JWT 签名;未配置时回退到 `SESSION_SECRET`,向下兼容
- 屏蔽手动升级入口(`/api/update/manual-upload`、`/api/update/manual-upgrade`),前端隐藏对应 UI 组件
---
## [v2.3.1] - 2026-06-03
### 变更
- 屏蔽手动升级入口,前端隐藏对应 UI 组件
- POW 与 WAF 规则合并, 统一逻辑处理
---
## [v2.3.0] - 2026-06-03
### 新增
- WAF IP 组支持订阅模式,可从远程文本或 JSON 源定时同步
- 新增 Pages 静态站点托管,支持 SPA fallback 路由配置
- Agent 实现 WebSocket 实时推送,Server 发布配置后立即通知在线 Agent
### 变更
- Agent 数据面与 OpenResty 合并为集成镜像部署方式
- 访问日志与观测数据支持数据库分片,按 ID 分片替代原有逻辑
---
## [v2.2.8] - 2026-06-03
### 修复
- 修复多域名部署场景下跨域认证绕过安全漏洞
---
## [v2.2.6] - 2026-06-02
### 新增
- 新增 Uptime Kuma 集成,支持自动同步监控任务
- WAF 新增 PoW(工作量证明)防护能力,可配置有效期
### 变更
- 内网穿透支持 TunnelRelay 中继节点(frps),新增 OpenFlared 客户端(frpc)
---
## [v2.2.5] - 2026-06-02
### 新增
- 新增 WAF 自动 IP 组,支持基于 Expr 规则定时聚合请求日志更新名单
- WAF IP 组黑白名单支持直接引用 IP 组对象
### 变更
- WAF 规则组与网站解耦,支持全局规则组和自定义规则组独立管理
---
## [v2.2.4] - 2026-06-02
### 新增
- WAF 规则组新增拦截返回配置 Tab
### 修复
- 修复 WAF 配置发布后部分规则不生效的问题
---
## [v2.2.3] - 2026-06-02
### 新增
- 新增 WAF 安全防护模块,支持 IP 黑白名单和地域拦截规则
---
## [v2.2.2] - 2026-06-01
### 变更
- 观测数据支持按时间窗口自动清理,新增数据库自动清理调度器
---
## [v2.2.1] - 2026-06-01
### 修复
- 修复仪表板概览数据压缩与规范化问题
---
## [v2.2.0] - 2026-06-01
### 新增
- 新增 TLS 证书转换为 ACME 托管证书的接口(`/convert-acme`)
- 新增 ACME 账号与 DNS 账号管理页面
- 支持 Let's Encrypt 自动申请与续期
---
## [v2.1.1] - 2026-06-01
### 变更
- Agent 架构调整,采用集成镜像方式内置 OpenResty
---
## [v2.0.3] - 2026-05-31
### 修复
- 修复版本号生成逻辑,确保使用当日最大序列号
---
## [v2.0.1] - 2026-05-30
### 修复
- 修复 GitHub 登录逻辑异常
---
## [v2.0.0] - 2026-05-30
### 新增
- 全面重构发布模型,引入配置版本不可变快照机制
- 支持配置版本回滚(重新激活旧版本)
- 新增 `source_config_json` 与 `support_files` 供 Agent 获取完整配置包
- 新增节点专属 Agent Token 与 Discovery Token 双轨鉴权
### 变更
- 数据库迁移框架切换至 goose,统一管理版本升级步骤
- Agent API 与管理端 API 鉴权完全分离
---
## [v1.9.3] - 2026-05-30
### 修复
- 修复节点 IP 自动探测逻辑,优先使用公网地址
---
## [v1.9.2] - 2026-05-29
### 变更
- Agent 心跳超时后自动退回 HTTP 轮询模式
---
## [v1.9.1] - 2026-05-29
### 修复
- 修复 Agent WebSocket 升级失败时的重连逻辑
---
## [v1.9.0] - 2026-05-29
### 新增
- Agent 支持 WebSocket 长连接,Server 发布后实时推送配置变更
---
## [v1.8.0] - 2026-05-26
### 新增
- 支持自定义 DNS 解析器(`OpenRestyResolvers`)
- 新增历史配置快照清理功能
### 变更
- CORS 配置支持动态源与凭证
- 上游统一渲染为命名 `upstream` 并启用 keepalive
---
## [v1.7.0] - 2026-05-25
### 新增
- 新增 ACME 和 DNS 账号管理功能,支持证书申请与续期
### 变更
- 移除新用户注册功能
- 更新 Go 版本要求至 1.25+
---
## [v1.6.1] - 2026-05-13
### 修复
- 修复个人设置页无法查看第三方认证源及解绑功能
---
## [v1.6.0] - 2026-05-13
### 新增
- 支持 OIDC 单点登录(SSO)
---
## [v1.5.0] - 2026-04-25
### 新增
- 集成 PoW(Anubis)防护,支持有效期配置
---
## [v1.4.0] - 2026-04-01
### 新增
- 支持域名级别独立绑定 TLS 证书,每个域名可单独选择证书
- 新增批量更新配置项接口
- 新增 Agent 卸载脚本
### 变更
- 禁用新用户自助注册
- 默认服务器块新增 HTTPS 握手拒绝支持
---
## [v1.3.2] - 2026-03-30
### 新增
- 网站配置支持多域名绑定与共享设置
- 新增抽屉式规则创建组件
---
## [v1.3.1] - 2026-03-20
### 新增
- 新增源站管理功能,支持源站创建、更新与删除
### 变更
- 重构代理路由页面,优化输入组件与样式
---
## [v1.3.0] - 2026-03-19
### 新增
- 新增数据库观测数据手动和自动清理策略
- 节点访问日志支持数据库分片,按 ID 分片
### 变更
- 数据库版本管理与迁移逻辑重构
---
## [v1.2.0] - 2026-03-19
### 新增
- 支持多上游地址负载均衡
- 新增缓存策略配置(路径前缀、精确路径)
- 节点健康事件清理功能
### 变更
- 上游渲染改为命名 upstream 并启用 keepalive
- 更新 HTTPS 配置,启用 reuseport 与 epoll 事件模型
---
## [v1.1.2] - 2026-03-18
### 变更
- HTTPS 启用 HTTP/2 支持
---
## [v1.1.1] - 2026-03-18
### 新增
- 新增获取配置版本详情 API
### 变更
- 仪表板概览数据结构优化,添加压缩与规范化
---
## [v1.1.0] - 2026-03-18
### 新增
- 新增应用日志分页查询与清理功能
- 新增访问日志 IP 汇总与趋势查询
- 新增 OpenResty DNS 解析器指令支持
- Docker 部署支持在运行中容器内执行 reload
### 修复
- 修复应用结果警告逻辑
- Lua 和证书文件管理重构,优化文件同步与清理机制
---
## [v1.0.2] - 2026-03-17
### 新增
- 支持 PostgreSQL 数据库,添加数据库迁移逻辑
- 新增 Docker Compose 配置,支持 PostgreSQL 联动部署
### 变更
- 多个管理端 API 请求方法从 PUT/DELETE 统一改为 POST
---
## [v1.0.1] - 2026-03-16
### 新增
- 新增 `origin_host` 字段,支持覆盖回源请求的 Host 头
### 修复
- 修复代理配置中 SSL 服务器名称和主机头覆盖逻辑
---
## [v1.0.0] - 2026-03-15
OpenFlare 首个正式版本发布。
### 新增
- 管理端 UI、管理 API、Agent API 基础功能
- 反向代理配置管理与 OpenResty 配置渲染
- 配置版本发布与 Agent 同步
- TLS 证书导入与管理
- 节点注册、心跳与状态观测
- SQLite 数据库支持
+17 -9
View File
@@ -10,7 +10,8 @@ export default defineAdditionalConfig({
sidebar: {
'/guide/': { base: '/guide/', items: sidebarGuide() },
'/reference/': { base: '/reference/', items: sidebarReference() },
'/design/': { base: '/design/', items: sidebarDesign() }
'/design/': { base: '/design/', items: sidebarDesign() },
'/changelog/': false
},
editLink: {
@@ -57,7 +58,8 @@ function nav(): DefaultTheme.NavItem[] {
return [
{ text: '指南', link: '/guide/', activeMatch: '/guide/' },
{ text: '参考', link: '/reference/', activeMatch: '/reference/' },
{ text: '设计', link: '/design/', activeMatch: '/design/' }
{ text: '设计', link: '/design/', activeMatch: '/design/' },
{ text: '更新日志', link: '/changelog/', activeMatch: '/changelog/' }
]
}
@@ -69,10 +71,13 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
{ text: '概览', link: '' },
{ text: '快速开始', link: 'quick-start' },
{ text: '基础使用', link: 'usage' },
{ text: '内网穿透与隧道使用', link: 'tunnel-usage' },
{ text: 'WAF 安全防护使用', link: 'waf-usage' },
{ text: 'WAF 自动 IP 组语法', link: 'waf-ip-group-expr' },
{ text: 'SSO 登录配置', link: 'sso' },
{ text: '发布第一份配置', link: 'first-site' },
{ text: '故障排查', link: 'troubleshooting' }
{ text: '故障排查', link: 'troubleshooting' },
{ text: '引用与致谢', link: 'credits' }
]
}
]
@@ -85,10 +90,12 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
items: [
{ text: '概览', link: '' },
{ text: '系统架构', link: '../design/architecture' },
{ text: '启动 Server', link: 'server' },
{ text: '接入 Agent', link: 'agent' },
{ text: '部署说明', link: 'deployment' },
{ text: '升级与维护', link: 'upgrade' },
{ text: '启动 Server', link: '../deployment/server' },
{ text: '接入 Agent', link: '../deployment/agent' },
{ text: '部署说明', link: '../deployment/deployment' },
{ text: '部署 Relay (Tunnel)', link: '../deployment/relay' },
{ text: '部署 OpenFlared', link: '../deployment/openflared' },
{ text: '升级与维护', link: '../deployment/upgrade' },
{ text: '配置项', link: 'configuration' },
{ text: '命令与脚本', link: 'cli' },
{ text: 'API 约定', link: 'api' }
@@ -104,8 +111,9 @@ function sidebarDesign(): DefaultTheme.SidebarItem[] {
items: [
{ text: '产品边界', link: '' },
{ text: '系统架构', link: 'architecture' },
{ text: '发布模型', link: 'release-model' },
{ text: '本地开发', link: 'development' },
{ text: 'Agent 与发布模型', link: 'agent-design' },
{ text: '内网穿透隧道设计', link: 'tunnel-design' },
{ text: 'WAF 设计', link: 'waf-design' },
{ text: '仓库结构', link: 'repository' }
]
}
@@ -13,7 +13,10 @@ OpenFlare Agent 运行在代理节点侧。它不会接收远程 shell 指令,
`agent_token` 与 `discovery_token` 至少填写一个。
[需要确认:当前管理端中创建或查看 `discovery_token` 与节点 `agent_token` 的准确菜单路径]
### 凭证获取路径
- **`discovery_token`(自动注册凭证)**:登录管理端后台,导航至「系统设置」->「自动注册」,在页面中可直接生成、查看和复制全局的自动注册凭证。
- **`agent_token`(节点专属凭证)**:登录管理端后台,导航至「节点管理」->「新增节点」,填写节点基本信息保存后,在节点详情页面即可直接复制该节点专属的接入 Token。
## 一键安装
@@ -89,7 +92,7 @@ curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/inst
}
```
如果不配置 `openresty_path`,Agent 默认调用 `openresty`。完整字段见 [配置项参考](./configuration.md#agent-配置字段)。
如果不配置 `openresty_path`,Agent 默认调用 `openresty`。完整字段见 [配置项参考](../reference/configuration.md#agent-配置字段)。
## Docker 运行
@@ -99,7 +102,7 @@ Docker 部署时直接运行内置 OpenResty 的 Agent 镜像:
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-p 80:80 -p 443:443/tcp -p 443:443/udp \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
@@ -2,10 +2,12 @@
你会学到:OpenFlare 的推荐部署方式、Server 与 Agent 的运行要求、源码启动方式、联调步骤、升级与卸载入口。
生产环境建议使用 PostgreSQL 作为 Server 数据库,并为 Server 显式配置 `SESSION_SECRET`。Agent 部署方式推荐为 Docker 部署(即直接使用内置 OpenResty 的 Agent 镜像);亦支持通过安装脚本或手动本地运行。
生产环境建议使用 PostgreSQL 作为 Server 数据库,并为 Server 显式配置 `JWT_SECRET`。Agent 部署方式推荐为 Docker 部署(即直接使用内置 OpenResty 的 Agent 镜像);亦支持通过安装脚本或手动本地运行。
## 部署拓扑
### 标准反代流量路径
```text
Browser
|
@@ -23,6 +25,26 @@ OpenResty binary
Origin service
```
### 内网穿透流量路径
```text
Browser
|
v
OpenResty (Agent, WAF/HTTPS 终结) <-- TunnelRelay 节点
|
| proxy_pass (127.0.0.1:{vhost_port})
v
OpenFlareRelay (frps 进程) <-- TunnelRelay 节点
|
| frp 隧道协议
v
OpenFlared (frpc 客户端) <-- 内网服务器
|
v
Internal Service (192.168.x.x)
```
## 前置条件
Server:
@@ -45,7 +67,14 @@ Agent:
| 网络 | Agent 节点必须能访问 Server 地址 |
| GeoIP | WAF 地域规则使用 Agent 本地 MaxMind mmdb;Agent 内置初始库并会定期更新 |
[需要确认:生产环境推荐的最低 CPU、内存与磁盘容量]
### 硬件配置推荐
| 组件 | 最低硬件配额 | 推荐硬件配额 | 说明 |
| --- | --- | --- | --- |
| **Server 控制面** | 1 核 CPU / 1 GB 内存 / 10 GB 磁盘 | 2 核 CPU / 4 GB 内存 / 50 GB+ 磁盘 | 磁盘用量需根据访问日志留存时长与并发流量合理扩容 |
| **Agent 数据面** | 1 核 CPU / 512 MB 内存 / 2 GB 磁盘 | 2 核 CPU / 2 GB 内存 / 10 GB+ 磁盘 | 根据 OpenResty 的并发代理连接量与 WAF 拦截处理扩容 |
| **Relay 中继节点**| 1 核 CPU / 1 GB 内存 / 5 GB 磁盘 | 2 核 CPU / 2 GB 内存 / 20 GB 磁盘 | frps 传输中继吞吐量主要受带宽与 CPU 吞吐能力限制 |
| **OpenFlared 客户端**| 1 核 CPU / 256 MB 内存 / 1 GB 磁盘 | 1 核 CPU / 512 MB 内存 / 5 GB 磁盘 | 独立运行于内网,自身资源占用极小,保障网络吞吐即可 |
## Docker Compose 部署 Server
@@ -78,7 +107,7 @@ services:
ports:
- "3000:3000"
environment:
SESSION_SECRET: replace-with-a-long-random-string
JWT_SECRET: replace-with-a-long-random-string
DSN: postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable
GIN_MODE: release
LOG_LEVEL: info
@@ -115,7 +144,7 @@ pnpm build
```bash
cd openflare_server
export SESSION_SECRET='replace-with-a-long-random-string'
export JWT_SECRET='replace-with-a-long-random-string'
export SQLITE_PATH='./openflare.db'
export LOG_LEVEL='info'
# 可选:设置后优先使用 PostgreSQL。
@@ -139,7 +168,7 @@ Docker 部署是 Agent 推荐的部署方式。Docker 部署时直接运行 Agen
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-p 80:80 -p 443:443/tcp -p 443:443/udp \
-v openflare-agent-data:/data \
-v ./agent.json:/etc/openflare/agent.json:ro \
ghcr.io/rain-kl/openflare-agent:latest
@@ -151,7 +180,7 @@ docker run -d --name openflare-agent --restart unless-stopped \
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-p 80:80 -p 443:443/tcp -p 443:443/udp \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
@@ -234,16 +263,6 @@ export LOG_LEVEL='info'
WAF 地域规则依赖 Agent 本地 `GeoLite2-Country.mmdb`。Agent 启动时会在 `data_dir/etc/openflare/GeoLite2-Country.mmdb` 初始化内置数据库,并按配置周期尝试更新;更新失败只记录警告,不影响配置同步与 OpenResty reload。
## 最小联调步骤
1. 启动 Server 并完成首次登录。
2. 在管理端准备 `agent_token` 或 `discovery_token`。
3. 启动 Agent,并确认节点在线。
4. 新增一条启用的网站配置。
5. 发布并激活新版本。
6. 查看节点详情和应用记录,确认版本应用成功。
7. 访问绑定域名或用 `curl` 验证反代结果。
## 升级与卸载
Server:
@@ -266,34 +285,3 @@ curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/unin
```
卸载脚本会停止 Agent、删除 systemd 服务和安装目录,不会删除本机 OpenResty。
## 常用验证命令
Server:
```bash
cd openflare_server
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Agent:
```bash
cd openflare_agent
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Frontend:
```bash
cd openflare_server/web
pnpm build
```
Swagger:
```bash
go install github.com/swaggo/swag/cmd/swag@v1.16.4
cd openflare_server
swag init -g main.go -o docs
```
+24
View File
@@ -0,0 +1,24 @@
# 部署与升级
本分区提供 OpenFlare Server、Agent、Relay 中继以及 OpenFlared 内网穿透客户端的详细部署指南、配置说明和升级维护步骤。
## 内容导航
### 快速开始
* **[快速开始](../guide/quick-start.md)**:5 分钟内使用 Docker Compose 启动 Server 和首个 Agent(推荐新用户)
### Server 部署
* **[启动 Server](./server.md)**:从源码构建前端、启动 Server、选择 SQLite 或 PostgreSQL
### Agent 部署
* **[部署 Agent](./agent.md)**:Agent 接入方式、Docker 部署、脚本安装、配置文件及故障排查
### Tunnel 内网穿透部署
* **[部署 Relay](./relay.md)**:TunnelRelay 节点的配置说明、Docker 部署与宿主机运行指南
* **[部署 OpenFlared](./openflared.md)**:内网穿透客户端配置说明、Docker 运行与自同步机制
### 升级与维护
* **[升级与维护](./upgrade.md)**:Server 与 Agent 升级步骤、数据清理策略、验证命令
### 参考资料
* **[部署说明](./deployment.md)**:部署拓扑、前置条件、Docker Compose 配置示例、多种部署方式综览
+119
View File
@@ -0,0 +1,119 @@
# 部署 OpenFlared 客户端
你会学到:OpenFlared 客户端的职责、配置参数与环境变量、基于 Docker 运行客户端的方法,以及如何在内网服务器上通过二进制方式独立部署。
**OpenFlared** 是部署在用户内网(局域网、私有云等无法被公网直接访问的环境)的隧道客户端。它的核心职责是通过 `X-Tunnel-Token` 与控制面(OpenFlare Server)建立通信,并在本地自动拉起并管理一个或多个 **frpc (快速反向代理客户端)** 进程,从而将内网的 HTTP 流量安全、稳定地穿透至外网的中继节点。
---
## 前置条件
1. **获取 Tunnel Token**:在 OpenFlare 管理端的「内网穿透」或「隧道管理」页面中,创建一个新的隧道实例,系统会自动生成唯一的 `tunnel_id` 与 `tunnel_token`(形如 `tun-<32hex>`)。
2. **网络出方向权限**:内网服务器无需任何公网入方向 IP 或端口映射,但必须能够通过网络访问公网上的 **OpenFlare Server 地址** 以及对应的 **TunnelRelay 节点中继端口 (默认 7000)**。
3. **软件依赖**(仅限宿主机直接部署):
- 本地需有可执行的 `frpc` 二进制文件(建议版本为 `v0.61.0+` 或最新稳定版 `v0.69.0`),或通过参数显式指定路径。
---
## 配置文件与环境变量
`openflared` 启动时默认会读取当前目录下的 `flared.json`。同时也完全支持通过环境变量进行覆盖。
### 配置字段详情
| JSON 字段 | 环境变量 | 说明 | 默认值 |
| --- | --- | --- | --- |
| `server_url` | `OPENFLARE_SERVER_URL` | OpenFlare Server 接口服务地址 | **无(必填)** |
| `tunnel_token` | `OPENFLARE_TUNNEL_TOKEN` | 隧道客户端专属认证 Token | **无(必填)** |
| `frpc_path` | `OPENFLARE_FRPC_PATH` | frpc 可执行二进制文件路径 | `"frpc"` |
| `data_dir` | `OPENFLARE_DATA_DIR` | 本地数据与生成的 `frpc_{relayNodeID}.toml` 存放目录 | `"./data"` |
| `state_path` | - | 本地状态记录文件路径(保存最后应用的配置版本)| `"{data_dir}/flared-state.json"` |
| `heartbeat_interval`| - | 状态心跳上报周期(支持毫秒数或 Go Duration 字符串) | `10000` (10s) |
| `sync_interval` | - | 隧道配置拉取同步周期(支持毫秒数或 Go Duration 字符串) | `30000` (30s) |
| `request_timeout` | - | 接口网络请求超时时长 | `10000` (10s) |
---
## Docker 运行(推荐)
Docker 部署是内网运行最简单也最安全的方式。官方的 `openflared` 镜像已经内置了客户端控制器以及 `frpc v0.69.0` 二进制运行时,无需额外搭建环境。
```bash
docker pull ghcr.io/rain-kl/openflared:latest
docker rm -f openflared 2>/dev/null || true
docker run -d --name openflared --restart unless-stopped \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_TUNNEL_TOKEN=YOUR_TUNNEL_TOKEN \
-v openflared-data:/app/data \
ghcr.io/rain-kl/openflared:latest
```
---
## 宿主机手动运行
如果您需要直接在内网的 Linux/macOS/Windows 宿主机上独立运行:
### 1. 编译二进制
```bash
cd openflared
go build -o flared ./cmd/flared
```
### 2. 准备 `flared.json`
在程序同级目录下创建 `flared.json` 配置文件:
```json
{
"server_url": "http://your-server-ip:3000",
"tunnel_token": "your-tunnel-auth-token",
"frpc_path": "/usr/local/bin/frpc",
"data_dir": "./data",
"heartbeat_interval": "10s",
"sync_interval": "30s"
}
```
### 3. 运行服务
```bash
export LOG_LEVEL='info'
./flared -config ./flared.json
```
---
## 启动与验证
### 1. 自动同步逻辑
启动成功后,OpenFlared 将执行以下工作流:
- **心跳与配置获取**:周期性向 Server 的 `/api/flared/heartbeat` 和 `/api/flared/config` 接口发起同步,验证 Token 并检测配置版本。
- **文件渲染**:当检测到配置版本(或校验和 Checksum)变化时,会自动拉取该隧道的完整路由规则。如果绑定了多个中继 Relay,将为每个 Relay 分别在 `data_dir` 下渲染出 `frpc_{relayNodeID}.toml`。
- **热重载或重启**:拉起对应的 `frpc` 子进程,或在配置文件发生改变时执行 `frpc reload` / 重启动作,以确保流量映射保持最新。
- **异常自恢复**:如果本地 `frpc` 隧道进程异常退出,主控程序会在 5 秒的退避惩罚后自动尝试重新启动。
### 2. 查看日志与连接状态
```bash
# Docker 容器日志
docker logs -f openflared
```
若进程运行无误,您会在日志中看到类似如下输出:
```text
flared config loaded ...
detected frpc version v0.69.0
flared process started
applying new tunnel config {"version": "...", "checksum": "..."}
frpc process missing, starting {"relay_id": "..."}
```
### 3. 管理端确认
打开管理后台的 **「内网穿透」** 页面:
- 查看对应隧道的在线状态,此时应当绿灯显示 **「在线」**。
- 您可以清晰地看到该隧道目前连接了哪些中继节点,以及各内网服务的穿透路由详情。
+126
View File
@@ -0,0 +1,126 @@
# 部署 Relay (Tunnel 中继)
你会学到:TunnelRelay 节点的职责、`openflare_relay` 的配置项与环境变量、使用 Docker 运行 Relay 的方法,以及如何通过源码手动构建并部署 Relay。
在 OpenFlare 的内网穿透体系中,**TunnelRelay 节点** 扮演着关键的角色。它与普通的边缘节点(Edge Node)不同,除了运行传统的 Agent(托管 OpenResty 进行 HTTPS/WAF 处理)外,还同机运行了 **Relay (frps 隧道管理器)** 服务,负责监听内网客户端(OpenFlared)的隧道连接并进行流量中继。
---
## 前置条件
在部署 TunnelRelay 节点之前,请确保:
1. **已注册为 TunnelRelay 类型节点**:在 OpenFlare 管理端「节点管理」中,添加一个类型为 `tunnel_relay` 的节点,并获取其专属的 `agent_token` 或使用全局 `discovery_token`。
2. **网络端口**:
- 必须确保 `bindPort`(frpc 连接端口,默认 `7000`)可被公网/内网客户端访问。
- 必须确保 `vhostHTTPPort`(HTTP Vhost 端口,默认 `8080`)处于空闲状态,Agent 将在此端口上与 frps 进行流量传递。
3. **软件依赖**(仅限宿主机直接部署):
- 本地需有可执行的 `frps` 二进制文件(建议版本为 `v0.61.0+` 或最新稳定版 `v0.69.0`),或通过参数显式指定路径。
---
## 配置文件与环境变量
`openflare_relay` 启动时默认会读取当前目录下的 `relay.json`。同时也完全支持通过环境变量进行覆盖。
### 配置字段详情
| JSON 字段 | 环境变量 | 说明 | 默认值 |
| --- | --- | --- | --- |
| `server_url` | `OPENFLARE_SERVER_URL` | OpenFlare Server 接口服务地址 | **无(必填)** |
| `agent_token` | `OPENFLARE_AGENT_TOKEN` | 节点专属 Token | 与下者二选一 |
| `discovery_token` | `OPENFLARE_DISCOVERY_TOKEN` | 自动注册 Token | 与上者二选一 |
| `node_name` | `OPENFLARE_NODE_NAME` | 节点标识名称 | 默认获取本机主机名 |
| `node_ip` | `OPENFLARE_NODE_IP` | 节点出口/监听 IP | 自动检测真实出口 IP |
| `frps_path` | `OPENFLARE_FRPS_PATH` | frps 可执行二进制文件路径 | `"frps"` |
| `data_dir` | `OPENFLARE_DATA_DIR` | 本地数据与生成的 `frps.toml` 存放目录 | `"./data"` |
| `state_path` | - | 本地状态 JSON 记录文件路径 | `"{data_dir}/relay-state.json"` |
| `heartbeat_interval`| - | 心跳周期(支持毫秒数或 Go Duration 字符串) | `10000` (10s) |
| `request_timeout` | - | 接口请求超时时长 | `10000` (10s) |
---
## Docker 运行(推荐)
Docker 运行是 TunnelRelay 节点最便捷的部署方案。官方镜像内置了 `openflare-relay` 控制器与 `frps v0.69.0` 运行时,开箱即用。
```bash
docker pull ghcr.io/rain-kl/openflare-relay:latest
docker rm -f openflare-relay 2>/dev/null || true
docker run -d --name openflare-relay --restart unless-stopped \
-p 7000:7000 \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
-v openflare-relay-data:/var/lib/openflare-relay \
ghcr.io/rain-kl/openflare-relay:latest
```
> [!TIP]
> 这里的 `-p 7000:7000` 映射的是 `frpc` 客户端连接中继的端口。如果管理端配置了自定义的 `relay_bind_port`,请对应修改宿主机端口映射。
---
## 宿主机手动运行
如果您倾向于在物理机或虚拟机上直接运行:
### 1. 编译二进制
```bash
cd openflare_relay
go build -o openflare-relay ./cmd/relay
```
### 2. 准备 `relay.json`
在程序同级目录下创建 `relay.json` 配置文件:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "your-relay-node-agent-token",
"frps_path": "/usr/local/bin/frps",
"data_dir": "./data",
"heartbeat_interval": "10s",
"request_timeout": "10s"
}
```
### 3. 运行服务
```bash
export LOG_LEVEL='info'
./openflare-relay -config ./relay.json
```
---
## 启动与验证
### 1. 查看进程日志
```bash
# Docker 容器日志
docker logs -f openflare-relay
```
如果是在 Linux 上通过 Systemd 托管的,可执行:
```bash
journalctl -u openflare-relay -f
```
### 2. 验证运行状态
启动成功后,Relay 将进行以下工作:
- 向控制面发送 HTTP 心跳以注册/上线。
- 从控制面获取最新的 frps 基础配置(包括 `bindPort`、`vhostHTTPPort` 与自动生成的隧道认证凭证 `auth_token`)。
- 在本地自动渲染出 `data/frps.toml` 配置文件。
- 自动拉起子进程 `frps -c data/frps.toml`。
- 如果进程意外崩溃,Relay 将在 2 秒后自动拉起它。
### 3. 管理端确认
登录管理后台,导航至 **「节点管理」**,确认:
- 该 TunnelRelay 节点状态标记为 **「在线」**。
- 节点类型正确标记为 **中继节点** 且 frps 运行状态为 **正常 (Healthy)**。
+172
View File
@@ -0,0 +1,172 @@
# 启动 Server
你会学到:如何从源码构建管理端前端、启动 OpenFlare Server、选择 SQLite 或 PostgreSQL,并访问 Swagger。
OpenFlare Server 是 Gin + GORM 单体控制面,负责管理端 UI、管理 API、Agent API、配置渲染、版本发布、数据存储与聚合查询。
## 前置条件
| 项目 | 要求 |
| --- | --- |
| Go | `1.25+` |
| Node.js | `18+` |
| pnpm | 推荐通过 `corepack enable` 使用项目声明的 pnpm |
| 数据库 | SQLite 文件目录可写,或可访问的 PostgreSQL 实例 |
生产环境必须显式配置 `JWT_SECRET`,并优先使用 PostgreSQL。
## 构建管理端前端
Go Server 会托管 `openflare_server/web/build` 中的静态产物。源码启动前先构建前端:
```bash
cd openflare_server/web
corepack enable
pnpm install
pnpm build
```
常用前端检查:
```bash
pnpm lint
pnpm typecheck
pnpm test
```
## 使用 SQLite 启动
```bash
cd openflare_server
export JWT_SECRET='replace-with-a-long-random-string'
export SQLITE_PATH='./openflare.db'
export LOG_LEVEL='info'
go run .
```
默认监听 `3000` 端口,访问:
```text
http://localhost:3000
```
## 使用 PostgreSQL 启动
```bash
cd openflare_server
export JWT_SECRET='replace-with-a-long-random-string'
export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable'
export LOG_LEVEL='info'
go run .
```
`DSN` 设置后优先于 SQLite。`DSN` 与兼容旧命名的 `SQL_DSN` 同时存在时,优先使用 `DSN`。
如果目标 PostgreSQL 数据库为空且本地 `SQLITE_PATH` 文件存在,Server 启动阶段会尝试把 SQLite 数据迁移到 PostgreSQL,并在日志中输出迁移进度。
## 使用 Docker 启动
使用 Docker 部署可以免去本地配置 Go 与 Node.js 前端构建环境的麻烦。OpenFlare 官方提供了完整的 Dockerfile 与 Compose 配置,支持独立容器启动及多服务联动部署。
### 1. 使用 Docker Run 极速启动(以 SQLite 为例)
确保当前目录下已创建用于持久化数据库和日志的数据卷目录。运行以下命令启动 Server:
```bash
# 创建本地挂载目录
mkdir -p ./openflare-data
# 启动容器
docker run -d \
--name openflare-server \
-p 3000:3000 \
-v $(pwd)/openflare-data:/data \
-e JWT_SECRET='replace-with-a-long-random-string' \
-e SQLITE_PATH='/data/openflare.db' \
-e GIN_MODE='release' \
-e LOG_LEVEL='info' \
openflare-server:latest
```
启动参数说明:
* **`-p 3000:3000`**:映射宿主机 `3000` 端口到容器内 `3000` 端口。
* **`-v $(pwd)/openflare-data:/data`**:挂载本地目录到容器的 `/data`,确保数据库文件 `openflare.db` 在重启或重建容器时不丢失。
* **`JWT_SECRET`**:管理端 API 登录令牌的 JWT 签名密钥,生产环境必须配置,避免重启后已登录令牌全部失效。
---
### 2. 使用 Docker Compose 一键启动(集成 PostgreSQL)
推荐在生产环境使用 Docker Compose,自动编排独立的 PostgreSQL 数据库并建立服务间的高可用关联。
在项目控制面目录下使用 `docker-compose.yaml` 进行编排:
```yaml
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: openflare
POSTGRES_USER: openflare
POSTGRES_PASSWORD: replace-with-strong-password
volumes:
- ./postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openflare -d openflare"]
interval: 10s
timeout: 5s
retries: 5
openflare:
image: openflare-server:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "3000:3000"
environment:
JWT_SECRET: replace-with-random-string
SQLITE_PATH: /data/openflare.db
DSN: postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable
GIN_MODE: release
LOG_LEVEL: info
volumes:
- ./openflare-data:/data
```
启动命令:
```bash
# 启动编排服务
docker compose up -d
```
Compose 参数说明:
* **`depends_on` 与 `healthcheck`**:通过 PostgreSQL 的健康度检查(pg_isready),确保数据库初始化完成并完全准备就绪后,再自动拉起 OpenFlare 控制面服务,避免首次连接数据库失败抛出 panic。
* **数据目录分离挂载**:`postgres` 数据挂载在 `./postgres-data`,`openflare` 数据与本地备份挂载在 `./openflare-data`,结构清晰,便于日常备份和维护。
## 命令行参数
```bash
go run . --port 3000 --log-dir ./logs
```
| 参数 | 作用 | 默认值 |
| --- | --- | --- |
| `--port` | 指定 Server 监听端口 | `3000` |
| `--log-dir` | 指定日志目录 | 空,输出到标准输出 |
| `--version` | 输出版本后退出 | `false` |
| `--help` | 输出帮助后退出 | `false` |
## 首次登录
默认账号:
| 用户名 | 密码 |
| --- | --- |
| `root` | `123456` |
首次登录后请立即修改默认密码。
@@ -49,37 +49,4 @@ journalctl -u openflare-agent -n 100 --no-pager
| `DatabaseAutoCleanupEnabled` | 是否启用每日自动清理 |
| `DatabaseAutoCleanupRetentionDays` | 自动清理保留天数,至少 1 天 |
开启后,Server 会在每天凌晨 3 点清理访问日志、指标快照与请求报告。
## 常用验证命令
Server:
```bash
cd openflare_server
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Agent:
```bash
cd openflare_agent
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Frontend:
```bash
cd openflare_server/web
pnpm lint
pnpm typecheck
pnpm test
pnpm build
```
Docs:
```bash
cd docs
pnpm build
```
开启后,Server 会在每天凌晨 3 点清理访问日志、指标快照与请求报告。
+175
View File
@@ -0,0 +1,175 @@
# Agent 设计文档
你会学到:Agent 的设计原则、核心功能模块、与 Server 的交互链路,以及如何通过不可变版本模型与三阶段容灾机制来保证配置应用的安全性和可靠性。
---
## 需求分析
在分布式反向代理与边缘安全网关场景中,Agent 扮演着打通控制面(Server)与数据面(OpenResty)的核心角色。由于 Agent 运行在用户实际的节点服务器上,其设计必须遵循以下核心安全与高可用需求:
1. **主动拉取(Pull 模型)而非被动接收**:Server 不直接持有节点的 SSH 秘钥,也不主动发起向节点的入向连接。所有控制指令与配置更新均由 Agent 主动通过心跳(Heartbeat)或长连接(WebSocket)向上拉取。这消除了节点侧的入向防火墙安全隐患,防止了控制通道被劫持。
2. **极低侵入性**:Agent 作为一个独立的 Go 二进制进程运行,只与本地 OpenResty 进程进行基于文件的配置重写与信号通知交互,不干涉节点上的其他系统服务。
3. **极强容灾与自愈能力**:由于网络抖动、磁盘写满或异常配置等因素极易导致配置同步失败,Agent 必须具备零依赖的本地回滚自愈能力,严防因单次配置失误导致整机服务彻底瘫痪。
4. **纯粹的数据与状态落地**:Agent 仅负责承载 Server 渲染好的文件与控制意图落地,不包含复杂的业务逻辑校验、多端租户鉴权等控制面职责,确保了节点侧的高效与轻量。
---
## 核心功能
Agent 主要由以下核心子模块组成,共同配合完成其完整的生命周期管理:
| 模块名称 | 对应目录 | 功能职责 |
| :--- | :--- | :--- |
| **配置同步** | `sync/` | 负责拉取完整配置包,写入文件,触发重载,记录并回报同步状态。 |
| **心跳管理** | `heartbeat/` | 定期向 Server 上报节点健康状态、资源指标,并获取最新激活版本摘要。 |
| **WebSocket** | `wsclient/` | 保持与 Server 的长连接,提供秒级实时的配置推送与控制面指令响应。 |
| **OpenResty 管控** | `nginx/` | 执行 Nginx 配置校验 (`openresty -t`)、重写、平滑重载 (`reload`) 及进程自启动。 |
| **本地状态库** | `state/` | 持久化记录本地应用版本、错误日志及未成功上报的可观测性指标缓冲。 |
| **自更新服务** | `updater/` | 监听 Server 自更新指令,安全拉取新版本二进制并完成原地热升级。 |
| **可观测性** | `observability/` | 采集系统宿主机 CPU/内存/磁盘及 Nginx 性能指标,处理访问日志并上报。 |
| **GeoIP 维护** | `geoipdata/` `geoipupdate/` | 维护并定期更新本地 GeoIP 数据库,为 WAF 地域过滤提供支撑。 |
---
## 与 Server 的交互链路
Agent 在生命周期中主要通过 **基于 Token 的自动注册** 和 **心跳/WebSocket 双通道** 与控制面通信。
### 1. 自动注册流程
若 Agent 启动时本地 `agent.json` 的 `access_token` 为空,但配置了 `discovery_token`,将触发自动注册流程:
1. Agent 向控制面 `/api/agent/register` 发送注册请求,携带本地硬件摘要、IP 及主机名。
2. Server 校验 `discovery_token` 有效后,在数据库生成唯一的 `NodeID` 与专属 `AccessToken`(即 `agent_token`)并返回。
3. Agent 将获取的专用 Token 写入本地配置文件,擦除一次性 `discovery_token`,后续所有的通信均基于专属 `AccessToken` 进行鉴权认证。
### 2. 双通道心跳与同步机制
* **HTTP 轮询通道(兜底与探测)**:Agent 默认按设定的 `heartbeat_interval` 间隔发送 POST 心跳包。上报指标的同时获取当前激活版本的摘要信息(Version & Checksum)。
* **WebSocket 通道(实时通信)**:在 HTTP 心跳成功后,Agent 自动尝试将连接升级为 WebSocket (`/api/agent/ws`)。
* WS 连接建立后,心跳与指标上报全面转移到 WS 管道,降低网络开销。
* Server 发布或激活新版本时,通过 WS 广播通知 Agent。Agent 收到变更事件后,**立即触发同步流程**,实现秒级配置生效。
* 若 WS 链路因网络问题断开,Agent 自动降级为 HTTP 轮询,并采用指数退避机制尝试重建 WS。
### 3. 交互时序图
```mermaid
sequenceDiagram
autonumber
participant Agent as OpenFlare Agent
participant OR as 本地 OpenResty
participant Server as OpenFlare Server
Note over Agent: 首次启动 (无 AccessToken)
Agent->>Server: 1. 自动注册请求 (携带 discovery_token)
Server-->>Agent: 2. 颁发 NodeID 与专属 AccessToken (agent_token)
Note over Agent: 存储 Token 至本地配置文件
rect rgb(240, 248, 255)
Note over Agent, Server: HTTP 兜底与 WebSocket 升级
Agent->>Server: 3. 发送 HTTP Heartbeat (上报系统状态与健康度)
Server-->>Agent: 4. 返回 ActiveConfig 摘要及 AgentSettings
Agent->>Server: 5. 发起 WebSocket 升级请求 (/api/agent/ws)
Server-->>Agent: 6. 升级成功 (建立双向持久实时通道)
end
rect rgb(245, 245, 245)
Note over Agent, Server: 实时配置发布应用链路
Note over Server: 管理员在 UI 点击发布配置
Server->>Agent: 7. 通过 WS 广播新配置摘要 (WSMessageTypeActiveConfig)
Agent->>Server: 8. 请求拉取完整配置详情 (携带目标 Version/Checksum)
Server-->>Agent: 9. 返回完整配置快照 (Nginx配置、证书、WAF规则等)
Note over Agent: 备份旧文件,写入新配置至本地临时路径
Agent->>OR: 10. 执行配置语法校验 (openresty -t)
OR-->>Agent: 11. 返回语法校验结果 (OK)
Agent->>OR: 12. 平滑重载信号 (openresty -s reload)
Agent->>Server: 13. 上报应用成功状态 (Apply Log & ActiveVersion)
end
```
---
## OpenResty 的管控
Agent 对数据面 OpenResty 的管控实现了端到端的闭环,包含配置落地、语法验证、平滑重载和异常状态捕获:
### 1. 配置文件的落地组织
同步成功后,Agent 会将配置按照特定的物理结构写入到本地 `/etc/nginx/openflare-lua/` 目录下(或配置指定的 `LuaDir`):
* `nginx.conf`:主配置文件(替换相关占位符,配置性能参数、Shared Dictionaries 及全局 Server)。
* `routes.conf`:路由配置文件(由 Agent 生成,包含所有代理网站的 Server 块、证书路径、缓存及速率限制指令)。
* `certs/`:证书存放目录(文件命名为 `{cert_id}.crt` 和 `{cert_id}.key`)。
* `waf/` 与 `pow/`:WAF 及防 CC 挑战所需的专用 Lua 运行时脚本。
* `waf_config.json` 与 `waf_ip_groups.json`:WAF 过滤引擎所需的结构化规则配置文件。
* `pages_dir`:Pages 静态站点部署目录,默认位于 `data_dir/var/lib/openflare/pages`。当激活配置引用 Pages 部署时,Agent 会下载部署 zip、校验 checksum、解压到部署 release 目录,并切换 `deployments/{deployment_id}/current` 供 OpenResty `root`/`try_files` 读取。
### 2. 精细化的重载动作
1. **备份当前配置**:在写入新文件之前,Agent 会将现有的配置文件复制到 `.backup` 临时目录下,保留完整的现场快照。
2. **写入并替换占位符**:将最新拉取的模板写入,自动将模板中的绝对路径占位符(如 `__OPENFLARE_LUA_DIR__`、`__OPENFLARE_PAGES_DIR__`)替换为本地实际运行路径。
3. **语法校验**:调用 `openresty -t -c <temp_nginx.conf>` 进行严格的语法测试。
4. **平滑重载**:若校验通过,将新配置移至正式路径,执行 `openresty -s reload`。若 OpenResty 处于未启动状态,则使用当前配置拉起进程。
5. **捕获异常**:校验或重载失败时,Agent 会截获标准错误输出(stderr),提取前 2000 个字符的详细报错信息。
---
## 发布与配置应用模型
OpenFlare 摒弃了动态 Patch 节点配置的落后方式,采用 **不可变配置版本发布模型**。
```text
修改规则 -> 预览 / 查看 diff -> 发布 -> 生成完整配置版本 -> 激活版本 -> Agent 拉取 -> 本地应用 -> 上报结果
```
### 1. 核心设计原则
* **完整发布**:每次发布均是对当前控制面所有启用路由、证书、Pages 部署引用、全局与局部 WAF 规则进行一次性全量编译,生成带唯一 `checksum` 的完整版本。
* **版本格式**:采用 `YYYYMMDD-NNN` 递增格式,确保版本历史直观、具备单调递增性。
* **全局单激活版本**:系统同时只有一个处于 `active` 状态的全局配置版本。回滚时无需逆向打补丁,只需将历史某个健康版本的状态改为 `active`,Agent 重新拉取应用即可。
### 2. 三阶段容灾回滚机制
当 Agent 发现配置应用(或平滑重载)失败时,将自动激活以下三阶段容灾防瘫痪链路:
```mermaid
graph TD
A[配置应用失败] --> B[第一阶段: 尝试本地备份恢复]
B -- 备份文件存在 --> C[写入本地备份文件]
C --> D[执行 openresty -t 校验]
D -- 校验成功 --> E[reload 恢复旧版本运行]
D -- 校验失败 --> F[进入第二阶段]
B -- 无备份 --> F[第二阶段: 写入内置安全兜底配置]
F --> G[写入兜底 nginx.conf: 仅监听 80 端口]
G --> H[启用 stub_status 健康检查]
G --> I[其他路由统一返回 503 且拦截异常配置]
G --> J[尝试拉起 OpenResty 维持基础存活]
J --> K[进入第三阶段]
E --> L[上报 Apply Warning]
K --> M[本地阻断该异常版本重复应用]
M --> N[上报 Apply Error 并保留详细报错]
```
1. **第一阶段:本地备份回退**
* Agent 尝试从前一步保存的 `.backup` 目录恢复主配置、路由及证书。
* 写入备份文件后,重新执行 `openresty -t` 校验。若成功,重载回退并向 Server 上报 `Warning`(警告:应用新版本失败,已自动退回历史健康版本)。
2. **第二阶段:内置安全兜底运行**
* 若本地不存在备份配置(如首次部署即配置错误),或者回退备份配置依然校验失败,Agent 将激活最终自愈机制——写入**内置安全兜底配置**。
* **安全兜底配置规范**:
* 仅监听 `80` 端口,不包含任何用户的真实反代路由。
* 除 `/openflare/stub_status` 健康监测路由返回正常外,其他一切访问请求统一返回状态码 `503 Service Unavailable`,响应体固定为 `OpenFlare: No Valid Configuration`。
* 尝试以此极简配置拉起 OpenResty。这能够确保 Nginx 进程自身不瘫痪,保留了底层的健康检查与探针通道,防止容器/Pod 因健康检查失败而被调度系统不断销毁重启,同时保护了敏感路由的安全性。
3. **第三阶段:本地配置阻断**
* Agent 会将当前导致崩溃的配置 `version + checksum` 记录在本地状态库的阻断名单中。
* 在控制面未激活新的配置(`checksum` 发生变化)之前,Agent 心跳将阻断对此异常版本的重复同步拉取,防止节点陷入“心跳 -> 拉取崩溃配置 -> 崩溃回滚”的死循环。
### 3. WAF IP 组运行时异步同步
为了避免高频变动的恶意 IP 黑名单频繁触发主配置的全量发布与 reload(平滑重载对 Nginx 依然有微小的 CPU 与连接开销),IP 组成员采用了与发布版解耦的**异步差分同步设计**:
* **静态发布快照**:发布生成的 `waf_config.json` 中仅包含规则组对 IP 组的引用关系(即 `ip_whitelist_group_ids` / `ip_blacklist_group_ids`),不包含具体的 IP 成员列表。
* **心跳差分对比**:Agent 在心跳包中上报本地已缓存 IP 组的 MD5 Checksum 映射表。
* **差分下发**:Server 比对当前激活版本引用的 IP 组哈希,仅向 Agent 下发缺失或发生变更的 IP 组成员,写入本地 `waf_ip_groups.json`,实现极速差分同步。
* **WebSocket 实时通知**:当 Server 手动更新 IP 组、订阅源自动同步成功、或安全规则自动触发临时封禁时,Server 会立即通过 WebSocket 广播受影响的 IP 组更新包,Agent 接收落地并即时生效,全程**无须 reload Nginx**。
---
## 设计约束
为保证数据与控制链路的安全边界,Agent 代码编写与二次开发必须严格遵守以下工程约束:
1. **零特权指令通道**:Server 绝对禁止向 Agent 传递任何任意 shell 命令或远程执行脚本(如 exec/eval 等)。所有系统控制原语(如启动、停止、重载、更新)必须硬编码在 Agent 二进制内部。
2. **严格的 Token 过滤与前缀验证**:Agent 侧向 Server 请求资源时,接口端点固定以 `/api/agent/` 为前缀,并强制携带 `X-Agent-Token` 进行签名或令牌核验。
3. **节点自治原则**:Agent 须具备完备的离线工作能力。在与 Server 失去连接期间,本地 OpenResty 必须依靠本地已落地的配置保持反向代理服务的绝对正常运行。
+26 -5
View File
@@ -48,13 +48,27 @@ OpenFlared (frpc) <-- 内网服务器
Internal Service (192.168.x.x)
```
### Pages 静态托管流量路径
```text
Browser
|
| HTTPS request
v
OpenResty (Agent, TLS/WAF)
|
| root/try_files
v
Agent 本地 Pages 部署目录
```
## 组件职责
| 组件 | 职责 |
| --------------- | ---------------------------------------------------------------------- |
| Server | 管理端 UI、管理 API、Agent/Relay/Client API、配置渲染、版本发布、数据存储与聚合查询 |
| Agent | 注册、心跳、同步、写入文件、校验、reload、失败回滚、自更新与轻量采集 |
| OpenResty | 接收真实流量,按 OpenFlare 渲染的配置执行 WAF、PoW、认证与反向代理 |
| Server | 管理端 UI、管理 API、Agent/Relay/Client API、配置渲染、版本发布、Pages 部署包存储、数据存储与聚合查询 |
| Agent | 注册、心跳、同步、写入文件、Pages 部署包拉取与解压、校验、reload、失败回滚、自更新与轻量采集 |
| OpenResty | 接收真实流量,按 OpenFlare 渲染的配置执行 WAF、PoW、认证、反向代理与 Pages 静态文件服务 |
| OpenFlareRelay | 管理 frps 进程生命周期,提供隧道中继服务,通过心跳接收 frps 配置 |
| OpenFlared | 管理 frpc 进程(可多个),连接 Relay 中继,将流量转发到内网服务 |
| Frontend | 管理网站配置、WAF、源站、证书、节点、Tunnel、版本、用户、设置与观测页面 |
@@ -65,12 +79,14 @@ Internal Service (192.168.x.x)
* Gin 提供 HTTP 服务。
* GORM 访问 SQLite 或 PostgreSQL。
* 现有登录体系提供管理端 Session。
* 现有登录体系签发管理端用户 Token,管理端 API 通过 `OPENFLARE_TOKEN` 请求头鉴权。
* 认证源与外部账号绑定支持 GitHub OAuth 和标准 OIDC。
* Go Server 托管 `openflare_server/web` 静态构建产物。
Server 不直接 SSH 到节点,也不在线修改节点文件。它只保存控制面状态、生成完整配置版本,并通过 Agent API 让节点主动拉取。
Pages 静态托管场景中,Server 保存 Pages 项目、SPA fallback 回退路径、不可变部署元数据、文件清单和 zip 部署包;发布版本只记录部署引用、checksum 与静态渲染策略,不把大体积静态资源写入 `config_versions`。
## Agent
`openflare_agent` 是 Go 单体程序:
@@ -79,6 +95,7 @@ Server 不直接 SSH 到节点,也不在线修改节点文件。它只保存
* 启动后读取或生成本地节点信息。
* 周期性 heartbeat,上报状态并获取激活版本摘要。
* 发现新版本后拉取配置、备份旧文件、写入新文件、校验并 reload。
* 当激活配置引用 Pages 部署时,先按部署 ID 下载 zip 包,校验 checksum,解压到本地 `pages_dir` 并切换当前部署目录。
* 应用失败时尝试恢复运行并回滚。
* 维护 WAF GeoIP mmdb,启动时写入内置初始库,并按配置定期更新。
@@ -118,6 +135,7 @@ Browser -> Frontend -> /api/* -> controller -> service -> model -> database
```text
Agent HTTP heartbeat -> Server 返回激活版本摘要
Agent 发现新版本 -> 拉取配置详情
Agent 确保 Pages 部署包已下载、校验并解压 (如配置引用 Pages)
Agent 写入主配置 / 路由配置 / 证书 / Lua 资源 / WAF 运行时配置
Agent 执行 OpenResty 校验与 reload
Agent 上报应用结果
@@ -180,6 +198,9 @@ WAF IP 组由 Server 管理。手动 IP 组直接保存 IP/IP 段列表;自动
* `proxy_routes`
* `origins`
* `config_versions`
* `pages_projects`
* `pages_deployments`
* `pages_deployment_files`
* `nodes`
* `tunnels`
* `auth_sources`
@@ -218,6 +239,6 @@ WAF IP 组由 Server 管理。手动 IP 组直接保存 IP/IP 段列表;自动
如果要修改架构相关代码,先阅读:
1. [产品边界](./index.md)
2. [发布模型](./release-model.md)
2. [Agent 与发布模型](./agent-design.md)
3. [开发约束](../guildline/development-constraints.md)
4. [仓库结构](./repository.md)
+4 -4
View File
@@ -39,7 +39,7 @@ SQLite 模式:
```bash
cd openflare_server
export SESSION_SECRET='dev-session-secret'
export JWT_SECRET='dev-jwt-secret'
export SQLITE_PATH='./openflare-dev.db'
export LOG_LEVEL='debug'
go run .
@@ -49,7 +49,7 @@ PostgreSQL 模式:
```bash
cd openflare_server
export SESSION_SECRET='dev-session-secret'
export JWT_SECRET='dev-jwt-secret'
export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable'
export LOG_LEVEL='debug'
go run .
@@ -101,7 +101,7 @@ export LOG_LEVEL='debug'
go run ./cmd/agent -config ./agent.json
```
未配置 `openresty_path` 时,Agent 默认调用 `openresty`。调试时可显式配置 `openresty_path`、`main_config_path`、`route_config_path`、`access_log_path`、`cert_dir`、`lua_dir` 和 `runtime_config_dir`。
未配置 `openresty_path` 时,Agent 默认调用 `openresty`。调试时可显式配置 `openresty_path`、`main_config_path`、`route_config_path` , `access_log_path`、`cert_dir`、`lua_dir` 和 `runtime_config_dir`。
## 测试
@@ -179,4 +179,4 @@ go build -o openflare-agent ./cmd/agent
4. 涉及配置、部署、API 或产品边界时同步更新文档。
5. 风险较高的修改补充测试或等效联调验证。
数据库结构变更必须提升数据库版本号,并补充从上一版本到新版本的显式迁移方法和校验逻辑。
数据库结构变更必须提升数据库版本号,并补充显式迁移方法和校验逻辑。v8-v17 保留为旧升级框架兼容链;v17 之后统一使用 goose,新的 goose 框架代码必须集中在 `openflare_server/model/goose` 包下;每次数据库升级都要在该包下新增独立的 `goose_<timestamp>_<description>.go` 文件,不得把具体迁移逻辑集中堆在 goose 注册入口中,也不得把新 goose 框架代码放回 `openflare_server/model` 根包。
+47 -56
View File
@@ -32,6 +32,7 @@ OpenFlare 当前不定位为通用日志平台、服务网格、Kubernetes Ingre
| 管理端前端 | 基于 Next.js 的正式管理端 |
| 认证源登录 | 支持以认证源形式配置 GitHub 与标准 OIDC 登录入口,并允许第三方账号绑定已有本地用户 |
| 内网穿透 | 通过 TunnelRelay 节点与 OpenFlared 客户端,将内网 HTTP 服务安全暴露到公网,复用 Agent 的 HTTPS/WAF 能力 |
| Pages 静态托管 | 以 Pages 项目管理静态站点部署包,发布后由边缘 Agent 拉取并在本地 OpenResty 静态服务 |
默认工作方式:
@@ -52,38 +53,47 @@ OpenFlare 当前不定位为通用日志平台、服务网格、Kubernetes Ingre
| 证书托管 | 为不同域名绑定 TLS 证书 |
| 基础观测 | 查看节点状态、请求聚合、访问分析和健康事件 |
| 内网穿透 | 通过 Tunnel 将无法直接公网访问的内网 HTTP 服务暴露到互联网,享有 HTTPS、WAF 等全部防护能力 |
| 静态站点托管 | 上传已构建的静态资源包,将网站规则上游绑定到 Pages 项目,在边缘节点本地服务静态文件 |
## 网站配置约束
`proxy_routes` 从“单域名规则”升级为“网站配置”聚合对象。一条记录对应一个网站,可绑定一个或多个域名,并共享一组站点级配置。
`proxy_routes` 是“网站配置”的聚合对象。一条记录对应一个网站,可绑定一个或多个域名,并共享一组站点级配置。
约束:
* `proxy_routes.site_name` 是网站的业务唯一标识。
* `proxy_routes.domains` 至少包含一个域名,且 `domains[0]` 作为主域名。
* 任一域名全局只能属于一个 `proxy_routes`。
* 迁移期可保留 `proxy_routes.domain` 作为 `domains[0]` 的镜像字段,但业务读写与后续扩展必须以 `site_name` + `domains` 为准。
* 网站级流量限制、反向代理与缓存配置当前按站点共享,不在同一网站内做域名级差异化配置。
* 网站级流量限制、反向代理与缓存配置均按站点共享,不在同一网站内做域名级差异化配置。
* HTTPS 允许在同一站点内按域名绑定证书。
## 源站约束
## 源站与上游约束
`origins` 只保存源站地址、展示名与备注,不承载协议、端口、路径、权重或健康检查策略。
`proxy_routes` 可选关联一个 `origins` 记录,用于复用源站地址;规则仍保存完整 `origin_url` 快照以参与渲染与版本快照。
`origins` 服务于源站目录复用,仅保存源站地址、展示名与备注,不承载协议、端口、路径、权重或健康检查策略。`proxy_routes` 可选关联一个 `origins`,但规则内部仍保存完整上游快照以参与渲染。
上游约束:
* `proxy_routes` 至少包含一个上游地址(直连类型),或关联一个 Tunnel(内网穿透类型)。
* `proxy_routes.upstream_type` 区分上游类型:`direct`(默认,直连)或 `tunnel`(内网穿透)。
* 为兼容历史数据保留 `origin_url` 主上游字段,也允许在同一规则内补充多个上游做负载均衡。
* 上游统一渲染为带 keepalive 的 named `upstream`。
* 单上游可附带 base path 或 query 并在 `proxy_pass` 中追加。
* 多上游限定为纯 `scheme://host[:port]`。
* `proxy_routes` 至少包含一个上游地址(直连类型 `direct`),或关联一个 Tunnel(内网穿透类型 `tunnel`),或关联一个 Pages 项目(静态托管类型 `pages`)。
* 多上游负载均衡统一渲染为带 keepalive 的 named `upstream`。
* 单上游允许附带 base path 或 query,并在 `proxy_pass` 中追加。多上游限定为纯 `scheme://host[:port]` 结构,且同一规则内的协议必须一致。
* `proxy_routes.origin_host` 为可选字段,用于回源时覆盖 `Host` 请求头。
* 所有直连类型上游地址都必须为合法 `http://` 或 `https://`。
* 内网穿透类型上游必须关联 `tunnel_id`,并指定内网目标地址与协议。
* 所有直连类型上游地址都必须为合法的 `http://` 或 `https://`。
* 内网穿透类型上游必须关联有效 `tunnel_id`,并指定内网目标地址与协议。
* Pages 类型上游必须关联有效 Pages 项目,且项目必须存在已激活部署。Pages 站点不执行服务端构建、边缘函数或动态运行时代码,仅托管预构建静态资源。
## Pages 静态托管约束
OpenFlare Pages 面向边缘节点静态站点托管,采用“项目 + 不可变部署 + 网站规则绑定”的模型。
约束:
* Pages 项目保存名称、标识、启用状态、SPA fallback 启用状态、自定义回退路径和当前激活部署。
* Pages 部署由管理端上传预构建 zip 包生成;部署包保存在 Server 本地 Pages 存储目录,数据库只保存部署元数据和文件清单,不保存大体积文件内容。
* 只有项目存在激活部署后,`proxy_routes.upstream_type = 'pages'` 的网站规则才能绑定该项目。
* Pages 网站继续复用网站规则的域名、HTTPS、WAF、PoW、Basic Auth、限流、缓存配置和配置版本发布机制。
* 发布快照保存 Pages 项目、部署 ID、部署 checksum、入口文件、SPA fallback 启用状态和回退路径。Agent 拉取激活配置时按部署 checksum 下载并校验部署包,解压到本地 `pages_dir` 后再应用 OpenResty 配置。
* V1 不支持 Git 自动构建、预览域名、边缘函数、动态 SSR、外部对象存储或多租户隔离。
## 内网穿透约束
@@ -147,12 +157,11 @@ OpenFlare 通过 TunnelRelay 节点与 OpenFlared 客户端实现内网穿透,
* **Tunnel 侧配置**:Relay 列表 + frpc 代理定义。随发布流程版本化,变更时优先使用 `frpc reload` 热重载。
* **Relay 配置**:通过心跳响应下发,相对静态,不纳入版本化流程。
### 当前阶段约束
### 隧道设计约束
* 仅支持 HTTP 协议隧道流量,保留未来 TCP/UDP 隧道扩展性。
* Tunnel 类型上游的域名 DNS 应仅解析到 TunnelRelay 节点;EdgeNode 上对应请求会因 frps 不可达返回 502。
* frp 版本使用 v0.61+(或更新稳定版),frp 二进制由部署脚本或 Docker 镜像提供。
* 暂不支持 TCP/UDP 端口分配;HTTP 单端口复用已满足 MVP 需求。
* 仅支持 HTTP 协议隧道流量(保留 TCP/UDP 隧道的可扩展性),暂不支持单独的 TCP/UDP 端口分配。
* Tunnel 类型上游的域名 DNS 应当解析到指定的 TunnelRelay 中继节点。
* frp 二进制(v0.61+)由系统部署脚本或容器镜像统一打包提供。
## HTTPS 约束
@@ -167,48 +176,30 @@ OpenFlare 通过 TunnelRelay 节点与 OpenFlared 客户端实现内网穿透,
## WAF 约束
WAF 以规则组为配置边界。系统固定一个全局规则组,默认应用到所有网站;网站可叠加多个自定义规则组。
WAF 以规则组为核心配置边界。系统提供唯一的全局规则组(默认应用至所有站点),网站可在此基础上叠加多个自定义规则组。
一期支持:
核心能力:
* IP / IP 段白名单与黑名单。
* IP 组引用,支持手动、自动、订阅三类 IP 组。
* 国家级地域白名单与黑名单。
* 规则组级拦截状态码与响应页面,默认 `418` 与空页面。
* 支持单个 IP / CIDR 网段黑白名单。
* 支持 IP 组引用(包括手动、自动Expr计算、URL订阅三类 IP 组)。
* 支持基于 GeoIP 的国家/地区级地域准入过滤。
* 支持规则组自定义拦截响应(支持自定义状态码与拦截 HTML 页面,默认返回 `418`)。
IP 组约束:
IP 组与判定约束:
* 手动 IP 组由管理端直接维护 IP/IP 段列表。
* 自动 IP 组使用 Expr 语法保存自定义规则,由 Server 定时按单个 IP 聚合请求日志并更新 IP 列表。
* 订阅 IP 组由 Server 定时从 HTTP/HTTPS URL 同步,支持文本列表和 JSON 映射。
* WAF 运行时不访问数据库;发布版本只保存规则组引用的 IP 组 ID,不把 IP 组成员展开进版本快照。
* Agent 通过心跳上报本地 IP 组 checksum,Server 仅返回 checksum 不一致的 IP 组;Server 侧 IP 组更新时会通过 Agent WebSocket 主动广播变更组,使节点可在不重新发布配置版本的情况下更新 WAF IP 组内容。
自动 IP 组首批内置预设规则:
* 单个 IP 请求数大于 100,且 404 状态码占比不低于 80%:`request_count > 100 && status_404_ratio >= 0.8`
* 单个 IP 通过 IP 地址访问次数大于 50,且通过 IP 地址访问占比大于 50%:`ip_host_count > 50 && ip_host_ratio > 0.5`
判定顺序:
* 白名单是放行例外,任意启用规则组命中白名单即放行。
* 未命中白名单时继续判断黑名单。
* 多个黑名单命中时,全局规则组优先,其后按自定义规则组 ID 升序。
地域识别由 Agent 维护节点本地 MaxMind mmdb,OpenResty Lua 在请求路径中读取本地库。GeoIP 依赖不可用时只能跳过地域规则,不得影响 IP 规则与反向代理主链路。
* **运行时解耦**:WAF 运行时只读取本地 JSON,不访问 Server 数据库;配置版本仅保存引用的 IP 组 ID。IP 组成员通过哈希 Checksum 差分心跳及 WebSocket 异步推送,实现无需平滑重载 Nginx 的热生效。
* **内置预设 Expr 规则**:
* 高频 404 扫描封禁:`request_count > 100 && status_404_ratio >= 0.8`
* 恶意 IP 直连探测:`ip_host_count > 50 && ip_host_ratio > 0.5`
* **判决优先级**:白名单拥有绝对优先权。若未命中白名单,则触发黑名单漏斗匹配(全局规则组优先,自定义组按 ID 升序匹配)。
* 地域解析依赖节点本地 MaxMind 库;当 GeoIP 异常时自动忽略地域规则,不得破坏 IP 规则与反代主链路的可用性。
## 认证源约束
`auth_sources` 是管理端第三方登录入口的配置对象,当前仅支持 `github` 与 `oidc` 两类。启用后的认证源会显示在登录页。
`auth_sources` 统一支持 `github` 与 `oidc` 登录配置入口。`external_accounts` 存储第三方与本地用户的绑定关系。第三方账号首次接入逻辑:
`external_accounts` 保存认证源外部账号与本地用户的绑定关系。第三方账号首次登录时:
* 已绑定本地用户则直接登录。
* 当前已有本地登录 Session 时,绑定到当前用户。
* 未绑定且允许注册时,自动创建普通用户并绑定。
* 未绑定且关闭注册时,只允许用户输入已有本地账号密码完成绑定。
旧 `users.github_id` 仅作为升级迁移来源,新的第三方账号登录与绑定关系必须以 `external_accounts` 为准。
* 已绑定时直接授权登录;若已有本地会话则自动建立绑定。
* 未绑定且允许注册时自动创建本地账号;若关闭注册,则要求用户提供已有本地账号密码以建立关联。
## 版本与观测约束
@@ -223,9 +214,9 @@ IP 组约束:
* 产品范围或系统边界变化时更新本文档。
* 系统结构或模块职责变化时更新 [系统架构](./architecture.md)。
* 发布、同步、回滚模型变化时更新 [发布模型](./release-model.md)。
* 发布、同步、回滚与 Agent 模型变化时更新 [Agent 与发布模型](./agent-design.md)。
* 开发约束、代码规范、接口约定变化时更新 [开发约束](../guildline/development-constraints.md)。
* 部署方式变化时更新 [部署说明](../reference/deployment.md) 与 README。
* 部署方式变化时更新 [部署说明](../deployment/deployment.md) 与 README.
* 配置项变化时更新 [配置项参考](../reference/configuration.md)。
* 已完成阶段不再以“版本计划”形式回填。
* 新阶段开始前,先补设计,再进入实现。
-79
View File
@@ -1,79 +0,0 @@
# 发布模型
你会学到:OpenFlare 为什么以完整配置版本为发布单位,发布、激活、Agent 应用和回滚分别如何工作。
OpenFlare 的发布模型以完整配置版本为中心,而不是在线修改节点配置。
标准链路:
```text
修改规则 -> 预览 / 查看 diff -> 发布 -> 生成完整配置版本 -> 激活版本 -> Agent 拉取 -> 本地应用 -> 上报结果
```
## 发布规则
Server 发布时必须:
1. 读取全部启用的 `proxy_routes`。
2. 读取 Server 侧 OpenResty 主配置、性能参数、缓存参数和必要 Lua 资源。
3. 读取域名与证书绑定关系。
4. 读取 WAF 全局规则组、自定义规则组、IP 组引用与网站绑定关系。
5. 保留 WAF 规则组引用的 IP 组 ID,渲染完整 OpenResty 配置与 WAF 运行时配置;IP 组成员不进入发布版本。
6. 计算 `checksum`。
7. 写入 `config_versions`。
8. 切换激活版本。
9. 让 Agent 在后续 heartbeat 中发现并应用。
版本号格式固定为 `YYYYMMDD-NNN`。
## 预览与发布
预览和 diff 是只读能力,不产生发布记录。
发布会生成新的完整配置版本。版本必须包含足够信息,让未来回滚时可以基于历史快照重新应用,而不依赖当前可变配置。
## 激活版本
全局同时只能有一个激活版本。当前不做按节点分组的差异化版本。
Agent 通过 heartbeat 获取激活版本摘要;当远端版本或 checksum 与本地状态不一致时,Agent 才进入同步流程。当 Agent WS 连接升级开启且连接可用时,Server 在发布或激活版本成功后会广播最新激活版本摘要,Agent 收到后复用普通同步流程立即拉取并应用配置。WS 不可用时仍按 HTTP heartbeat 间隔发现变更。
## 不可变历史
历史版本不可变。回滚不是修改旧版本,而是重新激活旧版本。
这样做的结果是:
* 每个版本都可以追溯。
* 回滚链路与普通发布应用链路一致。
* Agent 不需要理解“反向 patch”,只需要应用一个目标版本。
## Agent 应用策略
Agent 发现新版本后会:
1. 拉取目标版本详情。
2. 备份旧文件。
3. 写入主配置、路由配置、证书、必要 Lua 资源与 WAF/PoW 运行时配置。
4. 执行 OpenResty 配置校验。
5. reload;如果运行时未启动,则尝试用当前配置启动 OpenResty。
6. 上报成功、警告或失败。
如果新配置激活失败,Agent 必须尝试恢复运行;回滚成功时上报警告。若本地没有历史主配置可回滚,Agent 会写入内置安全兜底配置并尝试拉起 OpenResty:该配置对外只监听 `80` 端口,不包含任何用户路由,统一返回 `503 Service Unavailable` 与 `OpenFlare: No Valid Configuration`,同时保留本地 `stub_status` 健康检查入口。兜底启动成功时仍阻断失败目标版本并上报警告;存在历史主配置但回滚后仍无法恢复运行时上报失败。
某个目标 `version + checksum` 一旦应用失败并回退,Agent 会在本地状态中阻断该目标重复应用。只有远端激活版本或 checksum 发生变化,才允许再次尝试。
## 设计约束
* 发布必须读取全部启用的网站配置,而不是只渲染本次修改对象。
* 回滚通过重新激活旧版本实现,不修改历史版本。
* Agent API 固定使用节点专属 `agent_token`,首次接入可使用 `discovery_token`。
* Server 不提供远程 shell 或任意命令执行入口。
* 配置版本必须保存完整快照、渲染结果和 `checksum`。
* WAF 规则组、IP 组引用 ID 和网站绑定关系必须随完整配置版本进入快照与 checksum;IP 组成员由 Agent 独立按 checksum 差异同步,不受版本回滚影响。
## WAF IP 组运行时同步
WAF IP 组成员不纳入配置版本。发布版本只包含规则组直接 IP 与 `ip_whitelist_group_ids` / `ip_blacklist_group_ids`。Agent 应用版本后会从渲染出的 `waf_config.json` 中提取引用 ID,并向 Server 请求缺失或 checksum 不一致的 IP 组数据。
Agent 后续心跳会携带本地 IP 组 checksum。Server 根据当前激活版本引用的 IP 组 ID 对比 checksum,只返回差异组,避免每次心跳传输全部 IP 组。Server 在手动更新、订阅同步或自动规则执行后,会通过 Agent WebSocket 广播发生变化的 IP 组;WS 不可用时,下一次 HTTP heartbeat 仍会按 checksum 差异补齐。
+33 -1
View File
@@ -7,7 +7,9 @@
| `openflare_server` | Gin + GORM + SQLite/PostgreSQL 单体控制面 |
| `openflare_server/web` | Next.js 15 App Router 管理端前端,由 Go Server 托管 |
| `openflare_agent` | Go 单体 Agent,运行在节点侧 |
| `scripts` | Agent 安装、卸载等辅助脚本 |
| `openflare_relay` | Tunnel 中继代理,运行在公网边缘管理 frps 进程 |
| `openflared` | Tunnel 客户端,运行在内网服务器侧管理 frpc 进程 |
| `scripts` | 安装、自更新等系统辅助脚本 |
| `docs` | VitePress 文档站、设计基线、开发规范、部署与配置文档 |
| `docs/en` | 英文版文档 |
@@ -60,3 +62,33 @@
| `tests/` | 前端单元测试与集成测试(Vitest、Playwright) |
| `scripts/` | 构建和部署相关脚本 |
| `public/` | 静态资源 |
## Relay 模块
| 模块 | 职责 |
| ---------------- | ------------------------------------------------ |
| `cmd/` | Relay 命令行启动入口及初始化主函数 |
| `internal/config/`| 本地配置文件解析与默认参数初始化 |
| `internal/frps/` | 管理 frps 进程生命周期、端口与 Token 并监控运行 |
| `internal/heartbeat/`| 周期性 HTTP 心跳通信、上报状态并获取更新请求 |
| `internal/httpclient/`| Server 的通用 API 客户端调用工具类 |
| `internal/observability/`| 采集本地宿主机、frps 的基础运行指标并进行预聚合 |
| `internal/relay/` | 协调中继的核心生命周期、初始化与清理 |
| `internal/state/` | 本地运行时状态、错误记录与持久化缓存 |
| `internal/updater/`| Relay 升级检查、下载安装与重启机制 |
| `internal/wsclient/`| 与 Server 保持的长连接 WebSocket 双向通信管道 |
## OpenFlared (Client) 模块
| 模块 | 职责 |
| ---------------- | ------------------------------------------------ |
| `cmd/` | Client 命令行启动入口及初始化主函数 |
| `internal/config/`| 本地客户端配置加载与解析 |
| `internal/flared/`| 内网穿透客户端的核心调度与状态管理机制 |
| `internal/frpc/` | 热重载/动态生成多 Relay 的 `frpc.toml` 并监控 frpc |
| `internal/heartbeat/`| 与控制面进行的心跳通信,包含 Token 校验机制 |
| `internal/httpclient/`| 客户端通用 API 通信客户端 |
| `internal/sync/` | 增量拉取最新 Tunnel 路由绑定关系、生成快照并应用 |
| `internal/updater/`| 客户端自更新、新版检查与更新落地逻辑 |
| `internal/wsclient/`| 用于实时监听 Server 端隧道配置变更推送的 WS 信道 |
+130
View File
@@ -0,0 +1,130 @@
# 内网穿透隧道设计文档
你会学到:OpenFlare 内网穿透隧道的架构设计、双端管控组件(Relay 与 Client)的内部原理、交互逻辑以及数据面与控制面的通信流程。
---
## 需求分析
在典型的 Web 应用托管场景中,许多源站(Origin Server)部署在内网环境(如本地开发机、局域网服务器或受防火墙限制的内网集群)。这些服务器通常:
1. **无公网 IP**:无法直接被公网流量访问。
2. **安全合规限制**:不允许随意在边界路由器上配置端口映射(NAT)。
3. **动态 IP 变动**:传统的 DDNS 方案延迟高且极不稳定。
为了让内网源站能够无缝接入 OpenFlare 全局数据网关并享受 WAF 地域防护、TLS 证书托管等增值服务,OpenFlare 设计了基于 **反向中继穿透隧道** 的整体解决方案。在该架构中,公网边缘节点作为反代入口和流量中继,内网侧仅需发起安全出向连接,即可实现公网流量安全、稳定地反向穿透到内网源站。
---
## 核心功能
内网穿透隧道子系统包含以下核心能力:
* **Relay 节点动态管理**:由控制面动态派发中继服务(frps),动态分发服务端口与认证令牌(Token)。
* **多隧道反向代理映射**:支持在单个内网客户端上映射多个内网 Web 端口,并将多域名路由绑定至对应的中继节点。
* **独立进程生命周期管控**:中继与客户端均为 Go 编写的独立二进制守护进程,内部负责拉起、监控、自愈及热升级底层的 frp 引擎。
* **基于 Token 的独立认证隔离**:中继端使用 `agent_token`,内网客户端使用专属 `tunnel_token`,权限与路由边界隔离。
* **配置校验与增量热重载**:仅在隧道绑定关系、证书或 Relay 拓扑发生实际变化时,才重写配置文件并平滑重载进程,降低运行开销。
---
## 内网穿透与隧道架构
内网穿透子系统基于成熟的 `frp` 高性能隧道协议进行整合,分为 **控制面 (Control Plane)** 与 **数据面 (Data Plane)**。
```mermaid
graph TD
%% 数据流
Browser[1. 浏览器 / 访客] -->|HTTPS 请求| Agent[2. OpenResty / Agent]
Agent -->|本机转发 proxy_pass| RelayFrps[3. OpenFlare Relay / frps]
RelayFrps -->|加密隧道协议| FlaredFrpc[4. OpenFlared / frpc]
FlaredFrpc -->|转发本地请求| LocalOrigin[5. 内网源站 192.168.x.x]
%% 控制流与心跳
Server[OpenFlare Server 控制面] <-->|Relay API / Heartbeat| RelayManager[openflare_relay 进程]
Server <-->|Client API / Heartbeat| ClientManager[openflared 进程]
RelayManager -.->|管控进程及配置| RelayFrps
ClientManager -.->|管控多 Relay 进程| FlaredFrpc
style Browser fill:#f9f,stroke:#333,stroke-width:2px
style LocalOrigin fill:#9f9,stroke:#333,stroke-width:2px
style Server fill:#f96,stroke:#333,stroke-width:2px
```
* **控制面(Control Plane)**:Server 维护数据库状态;中继节点上的 `openflare_relay` 进程与内网服务器上的 `openflared` 进程通过 HTTP 心跳与 WebSocket 长通道同步隧道配置。
* **数据面(Data Plane)**:公网流量首先进入公网边缘的 Agent (OpenResty),在此完成 HTTPS 握手、TLS 终止和 WAF 过滤,接着通过 `proxy_pass` 转发到同机部署的 `openflare_relay (frps)`。`frps` 再将请求封包通过与内网 `openflared (frpc)` 建立的持久隧道传输过去,最后由 `frpc` 拆包并分发给内网实际的源站服务。
---
## Relay (中继端) 设计
`openflare_relay` 是部署在公网边缘的中继管理器,运行在 `tunnel_relay` 类型的节点上。
### 1. 核心架构与逻辑
* **进程守护**:Relay 进程内部持有 `frps` 二进制,通过 `exec.Command` 拉起 `frps -c frps.toml` 子进程,并启动 goroutine 异步监听其退出状态。如果发现 `frps` 异常退出,会结合退避机制自动拉起。
* **动态配置渲染**:通过 HTTP 心跳向控制面同步状态,获取当前的 `RelayConfig`,主要参数包括:
* `bindPort`:frps 用于监听内网 frpc 客户端连接的公网控制端口。
* `vhostHTTPPort`:虚拟主机(Virtual Host)HTTP 流量监听端口,Agent 的 proxy_pass 会指向此端口。
* `authToken`:客户端连接时进行握手校验的安全凭证。
* `webServer`:开启 frps 的仪表盘 API,Relay 基于此接口或管理控制端口收集实时的活跃隧道数和流量指标。
* **状态上报**:Relay 每周期心跳会向控制面上报底层 `frps` 的活跃连接数、注册客户端数、各个代理通道的实时状态以及 Relay 版本。
---
## Openflared (客户端) 设计
`openflared` 是运行在用户内网服务器侧的客户端管理器,使用独立的 `tunnel_token` 进行鉴权。
### 1. 核心设计机制
* **多 Relay 支持(多路复用)**:
为保障高可用或就近接入,控制面可能会将客户端连接调度到多个公网 Relay。`openflared` 会读取 `TunnelConfig` 中下发的 Relays 列表,在本地为每一个 Relay 节点独立生成一个专用的配置文件(命名为 `frpc_<relay_node_id>.toml`),并分别为每个 Relay 进程分配独立的 cancelable context。
* **子进程独立监控**:
`openflared` 内部维护一个 `processes` 映射表,对每个 `frpc` 子进程进行独立的生命周期管控。当控制面增加或移除 Relay 时,客户端会增量拉起新进程或优雅注销老进程,避免影响其他正常工作的隧道。
* **动态 TOML 生成**:
为每个 Relay 渲染 TOML 时,客户端会遍历 Proxies 列表,将每个内网服务的 `LocalAddr`、`LocalPort`、绑定的 `CustomDomains` 写入到 `[[proxies]]` 块中。
---
## 交互逻辑与流量模型
内网穿透子系统实现了一致性版本控制和状态反馈。
### 1. 控制面发布与同步流程
```text
管理员修改隧道/内网端口映射 -> 提交发布 -> 生成新 Tunnel 版本与 Checksum
|
v (推送或心跳拉取)
+-------------------------------------------+-------------------------------------------+
| |
v (中继端) v (内网客户端)
openflare_relay 心跳检测到 frps 端口/Token 变化 openflared 心跳检测到 tunnel_version 发生变更
重新渲染本地 frps.toml 请求拉取最新代理映射包
Kill 并重新拉起 frps 进程 重新渲染 frpc_<relay_id>.toml
上报健康状态为 healthy 对有变更的 Relay 进程执行重启与配置热重载
上报应用结果 (Apply Success/Error)
```
1. **版本化控制**:所有内网隧道的路由和映射关系与主路由系统类似,也经过版本化控制,下发 `version` 与 `checksum`,确保客户端不重复写入和频繁重载进程。
2. **应用结果闭环**:客户端应用新配置后,会在心跳中携带应用结果上报控制面。若因内网端口不可达或证书配置有误导致 frpc 无法建连,客户端会截获进程输出将 `LastError` 上报,管理员在 Server 即可直观查看穿透失败原因。
### 2. 数据面流量模型
1. **公网入口 (Agent)**:
```nginx
server {
listen 443 ssl;
server_name intranet.example.com;
# ... TLS 证书与 WAF 过滤逻辑 ...
location / {
proxy_pass http://127.0.0.1:18080; # 指向本地 frps 的虚拟主机端口
proxy_set_header Host $host; # 必须保留原 Host,因为 frps 依靠 Host 进行内部路由分发
proxy_set_header X-Real-IP $remote_addr;
}
}
```
2. **中继节点 (frps)**:
`frps` 监听到 `18080` 端口有 HTTP 请求进来,读取 HTTP 请求头中的 `Host: intranet.example.com`,在其已注册的活跃隧道表中检索该域名对应的加密 TCP 连接(由内网 frpc 建立)。
3. **加密隧道传输 (TCP)**:
`frps` 将 HTTP 请求封装进内部 TCP 隧道协议,发送给内网的 `frpc` 客户端。
4. **内网客户端分发 (frpc)**:
`openflared` 管理的 `frpc` 收到封包,根据本地配置(`localIP = "127.0.0.1"`, `localPort = 8080`)将请求建立本地 TCP 连接转发给内网 Web 服务,并将 Web 服务的响应原路打包返回,最终呈现给公网用户。
+130
View File
@@ -0,0 +1,130 @@
# WAF 设计文档
你会学到:OpenFlare 边缘 Web 应用防火墙(WAF)的核心架构、动态 IP 组异步差分同步模型、OpenResty Lua 高性能缓存方案以及完整的请求过滤与判定逻辑。
---
## 需求分析
在互联网公开环境中,Web 应用程序面临着各种各样的安全威胁(如扫描器踩点、刷接口、针对特定地域的恶意网络爬虫、勒索攻击及 CC 攻击等)。如果直接把恶意请求放行给源站(Origin Server),会导致:
1. **源站负载飙升**:高频的数据库查询与 CPU 运算极易耗尽服务器资源。
2. **敏感接口被刷**:登录、注册、短信验证码接口容易被恶意滥用导致财产损失。
3. **数据泄露风险**:恶意的通用漏洞探测行为无法被提前拦截。
因此,OpenFlare 需要在最前端的数据面(OpenResty)构建一套 **高性能、可弹性伸缩的 WAF 过滤引擎**。该引擎能够在最接近用户的边缘层以毫秒级的极低开销对恶意请求进行深度过滤,减轻源站压力,并提供防 CC(PoW 挑战)、IP 黑白名单与地域级别拦截等核心安全防护能力。
---
## 核心功能
OpenFlare WAF 包含以下核心防护维度:
* **IP 级拦截(IP 黑白名单)**:支持单 IP、CIDR 网段过滤,支持将上万 IP 聚合为 IP 组进行高效比对。
* **地域黑白名单(GeoIP 限制)**:集成 MaxMind 数据库,支持针对国家(Country)和省份/地区(Region)执行精准准入控制。
* **自定义拦截响应**:支持针对不同的过滤规则自定义阻断状态码(如 403, 418)以及个性化的 HTML 拦截页面。
* **人机挑战(PoW CC 防护)**:支持无感人机挑战,通过计算 Hash 碰撞防止自动化脚本和僵尸网络(Botnet)对接口进行并发冲击。
---
## IP 组设计与动态异步同步
IP 组是 WAF 进行高效黑白名单管控的核心容器。OpenFlare 将 IP 组根据更新频率与产生渠道分为三类:
### 1. IP 组类型
* **手动 IP 组(Manual)**:由管理员在控制面板上手动输入 IP 或 CIDR 列表。主要用于静态的信任 IP 或长期的封禁。
* **订阅 IP 组(Subscription)**:配置远程文本(按行分隔)或标准的 JSON 订阅地址。Server 侧的定时任务会周期性抓取远程订阅源并自动解析导入。主要用于集成开源的威胁情报库、云厂商的 IP 范围等。
* **自动 IP 组(Automatic)**:**最具弹性的动态防护通道**。控制面的定时扫描任务会读取所有节点的访问日志,按照设定的 Expr 规则(例如:“5分钟内请求 `/api/login` 接口触发 401 超过 50 次”)进行聚合分析,一旦匹配,自动将该恶意源 IP 写入封禁组,并指定封禁时长。
### 2. 异步差分同步设计 (不触发 Nginx Reload)
在传统的 Nginx WAF 设计中,IP 黑名单的更新通常需要重写配置并 reload。如果恶意 IP 封禁以秒级或分钟级高频触发,频繁 reload 会导致 Nginx 频繁新建 Worker 进程并销毁老进程,导致性能骤降。
OpenFlare 采用 **动态 IP 组异步差分同步设计**:
```text
WAF IP 成员更新 (手动/订阅/自动自动触发)
|
v
Server 更新数据库并计算该 IP 组的全新 MD5 Checksum
|
+----------------------------------------+
| (WebSocket 实时广播) | (心跳兜底比对)
v v
Server 立即向所有 Agent 推送变更组的完整成员 Agent 心跳上报本地所有 IP 组的 Checksum 映射表
| |
| v
| Server 发现 Checksum 不一致,下发变更的 IP 组成员
v |
Agent 接收成员数据,将其以 JSON 形式写入本地磁盘路径:waf_ip_groups.json
|
v (Lua 内存感知)
OpenResty Lua 引擎通过 MD5 校验和秒级感知文件变化并热更新内存,无需 reload 进程
```
通过这一架构,上万个高频变动的动态黑名单 IP 的落地和生效,**全程无需 reload 任何 Nginx 进程**,极大地保护了网关的高并发性能。
---
## 规则组与网站绑定
* **WAF 规则组(Rule Group)**:WAF 过滤政策的最小逻辑集合。一条规则组内可以包含 IP 黑白名单、IP 组引用、地域限制及防 CC 挑战配置。
* **全局规则组(Global)**:当规则组被标记为 `is_global = true` 时,该规则组对节点上托管的**所有网站路由**默认生效。
* **网站绑定绑定(Site Binding)**:网站路由(Proxy Route)可以绑定一个或多个非全局规则组。判定时,会执行 `全局规则组 + 绑定规则组` 的并集逻辑。
---
## 实现方案与高性能缓存
WAF 在 OpenResty 的 `access_by_lua` 阶段被触发,核心由 Lua 文件与本地落地的 JSON 配置构成。
### 1. 物理结构
* `waf_config.json`:包含所有规则组的元数据、国家地域限制、以及网站(Site)与规则组的关联映射。
* `waf_ip_groups.json`:包含所有同步下来的 IP 组与对应的 IP 列表。
* `waf/runtime.lua`:WAF 规则比对的实际运行时引擎。
* `waf/check.lua`:接入层入口,负责包引入与 check() 触发。
### 2. 共享内存字典 (ngx.shared) 高性能缓存设计
在每次 Web 请求进来时都读取磁盘上的 JSON 文件并进行解码,会导致磁盘 I/O 成为严重的性能瓶颈。
OpenFlare 利用 **OpenResty 共享内存字典 (ngx.shared.openflare_waf_config)** 设计了二级缓存机制:
1. **零文件 I/O 路径**:
在 Lua 中,每次执行 `check()` 时,首先利用 `ngx.md5` 瞬间计算本地磁盘 JSON 文件的 MD5 哈希(这一操作几乎为零耗时,因为文件已被操作系统 Page Cache 缓存)。
2. **哈希比对与热加载**:
比对共享内存中存储的缓存哈希键(`_config_hash`)。
* **若哈希未发生变化**:直接从共享内存字典中读取已解码、存在内存中的 Lua Table 配置,整个校验过程完全基于**共享内存操作**,耗时在 **微秒级** 级别。
* **若哈希不一致**:说明 Agent 刚刚落地了新的 WAF 规则或 IP 组,Lua 自动读取磁盘文件并使用 `cjson.decode` 解码,解码后的数据及全新的 MD5 写入共享内存,供后续 Worker 进程无缝读取。
---
## 应用流程与判定判定控制逻辑
当一个 HTTP/HTTPS 请求到达 OpenResty 后,WAF 会在 `access` 阶段按下图所示的漏斗判决链进行逐步匹配拦截:
### 1. WAF 判定流程图
```mermaid
flowchart TD
A[请求进入 access 阶段] --> B[获取当前请求的 Site Name]
B --> C[在共享内存中加载与此 Site 绑定的所有活跃规则组]
C --> D{匹配到 IP 白名单 / 白名单 IP 组?}
D -- 是 (匹配成功) --> E[放行请求 - ALLOW]
D -- 否 --> F{匹配到国家/地区地域白名单?}
F -- 是 (匹配成功) --> E
F -- 否 --> G{匹配到 IP 黑名单 / 黑名单 IP 组?}
G -- 是 (匹配成功) --> H[阻断请求 - BLOCK]
G -- 否 --> I{匹配到国家/地区地域黑名单?}
I -- 是 (匹配成功) --> H
I -- 否 --> J{是否启用了防 CC PoW 验证?}
J -- 是 --> K[转交防 CC 模块处理]
J -- 否 --> L[无安全风险,正常放行]
H --> M[退出并返回规则组配置的自定义状态码与拦截响应体]
```
### 2. 判决步骤细则
1. **白名单前置**:
为了防止误杀以及保障核心回源流量(如搜索引擎蜘蛛、CDN 回源 IP、办公区出口)的顺畅,WAF **优先匹配 IP 白名单与地域白名单**。一旦白名单匹配成功,直接绕过后续的所有黑名单检测和 CC 挑战,立刻放行。
2. **黑名单强力阻断**:
如果在白名单判定中未被捕获,请求将进入黑名单漏斗。一旦请求源 IP 命中 IP 黑名单、命中引用的黑名单 IP 组、或是处于被禁止的国家/地区范围内,Lua 引擎立即将 `ngx.ctx.openflare_waf_blocked` 标记设为 `true`。
3. **输出响应**:
命中黑名单后,Lua 提取匹配到规则组的 `block_status_code`(默认返回 418 / 403)和 `block_response_body`(拦截页面 HTML),通过 `ngx.say()` 输出响应体并执行 `ngx.exit(status)` 平滑退出请求,防止请求继续向后透传。
-188
View File
@@ -1,188 +0,0 @@
You are a senior Go backend engineer responsible for maintaining and developing a long-evolving Go application.
Your goal is not to "write code as quickly as possible," but to produce high-quality code that is maintainable, testable, evolvable, and conforms to Go ecosystem practices. It is forbidden to pile up temporary code, over-abstract, duplicate logic, or break the existing architecture just to complete tasks.
Before any development, you must first read and understand the existing code structure, including:
- Project directory structure
- Entry files
- Configuration management methods
- Database/cache/message queue access methods
- HTTP/RPC/API layer design
- Layering methods such as service/usecase/domain/repository
- Error handling methods
- Logging methods
- Test organization methods
- Dependency injection methods
- Existing coding style
If you are unsure of the responsibility of a certain module, infer it from the code context first; do not arbitrarily create duplicate modules.
Development Principles:
1. Architecture First
- Prioritize integrating into the existing architecture rather than starting from scratch.
- Do not arbitrarily add global variables, init side effects, or implicit dependencies.
- Do not write business logic into handlers/controllers.
- Handlers are only responsible for parameter parsing, authentication contexts, calling usecases/services, and returning responses.
- Services/usecases are responsible for business orchestration.
- Repositories/daos are responsible for data access.
- Domain/models are responsible for core business objects and rules.
- Isolate infrastructure code from business code.
2. Go Style
- Use clear, direct, and simple Go code.
- Do not mimic Java-style over-abstraction.
- Interfaces should be defined by the consumer, not forced by the provider.
- Prioritize small interfaces.
- Naming must be accurate. Do not use vague names like Manager, Helper, or Util unless absolutely necessary.
- Keep functions short and single-responsibility.
- Do not introduce generics, reflection, or complex design patterns just to "look advanced."
- Do not hide errors.
- Errors must contain context information; use `fmt.Errorf("...: %w", err)` when necessary.
- Do not panic, except for unrecoverable errors during the program startup phase.
3. Maintainability
- Analyze the scope of impact before making modifications.
- Keep changes minimal and avoid unrelated refactoring.
- Do not change public APIs, database structures, or configuration formats unless explicitly requested by the task.
- If changes must be made, explain the compatibility impact and migration plan.
- Confirm there are no callers before deleting code.
- Avoid copy-pasting existing logic; extract it to an appropriate place, but do not over-abstract.
- Add necessary comments to complex business logic to explain "why", not the obvious "what".
4. Testing Requirements
- New business logic must be supplemented with unit tests.
- Bug fixes must be supplemented with regression tests.
- Tests should cover normal paths, exceptional paths, and boundary conditions.
- Do not break the structure of business code for testing convenience.
- Isolate external dependencies using mocks/fakes/stubs.
- Name tests clearly, e.g., TestXXX_WhenYYY_ShouldZZZ.
- Prioritize table-driven tests, but do not sacrifice readability for table-driven structure.
5. Concurrency and Resource Management
- Goroutines must have exit mechanisms.
- Where context is involved, context.Context must be passed correctly.
- Do not arbitrarily use context.Background() to replace upstream contexts.
- Channels must have clear responsibility for closing.
- Keep lock scopes small to avoid deadlocks.
- Correctly close resources such as HTTP, databases, files, and connections.
- Pay attention to race conditions, goroutine leaks, and connection leaks.
6. Database and Transactions
- Database access must be in the repository/dao layer.
- Transaction boundaries should be controlled by the business use case layer, rather than scattered across multiple lower-level functions.
- Do not produce obviously inefficient N+1 queries in loops, unless the data volume is controllable and explained.
- SQL must be readable and parameterized; unsanitized inputs are strictly forbidden from concatenation.
- Schema changes must consider migration, rollback, and compatibility.
7. API Design
- Request parameters must be validated.
- Error responses must be stable and clear, without leaking internal sensitive information.
- Do not print sensitive data such as passwords, tokens, keys, or ID numbers in logs.
- Keep return structures backward-compatible.
- HTTP status codes must be semantically correct.
8. Logging and Observability
- Key paths must have necessary logs.
- Error logs must contain the context needed for troubleshooting, but must not leak sensitive data.
- Do not print logs excessively.
- Do not use fmt.Println directly in library code.
- If the project already has a logger, use the existing logger uniformly.
9. Security Requirements
- All external inputs are untrusted.
- Do not hardcode keys, tokens, or passwords.
- Do not commit sensitive configurations to the repository.
- Be mindful of injection risks in file paths, URLs, command executions, SQL, template rendering, etc.
- Authentication and permission checks must be placed in clear locations, and must not rely on the self-discipline of the frontend or callers.
10. Performance Requirements
- Do not optimize prematurely.
- However, do not write obviously inefficient code.
- Avoid unnecessary memory allocations, large object copies, and repeated parsing on hot paths.
- Page, stream, or batch operations should be considered for large data processing.
- If caching is introduced, the consistency, expiration strategy, and invalidation conditions must be explained.
Workflow:
Every time you receive a development task, you must follow these steps:
Step 1: Understand Requirements
- Briefly rephrase the requirements in your own words.
- Clarify inputs, outputs, boundary conditions, and exceptional cases.
- If requirements are vague, list your reasonable assumptions; do not write code blindly.
Step 2: Read Existing Code
- Identify relevant modules, call chains, data structures, interfaces, and tests.
- Explain how the current code works.
- Determine which layer the modification should be placed in.
Step 3: Design Solution
- Provide a minimal viable modification plan.
- Explain why it is placed in these files/modules.
- State whether it affects existing APIs, databases, configurations, or tests.
- If there are multiple solutions, compare their pros and cons and select the more stable one.
Step 4: Coding
- Only modify code related to the task.
- Maintain the existing code style.
- Do not introduce unnecessary new dependencies.
- Do not create duplicate logic.
- Do not leave TODOs, temporary code, or debugging code.
Step 5: Testing
- Supplement or update tests.
- Explain what scenarios the tests cover.
- If tests cannot be run, explain why and give the commands that should be run.
Step 6: Delivery Explanation
- Summarize what was modified.
- Explain why it was modified this way.
- Explain potential risks.
- Provide verification methods.
- If there are incomplete items, they must be explicitly listed; do not pretend they are complete.
Output Format:
Each of your replies should contain:
1. Requirements Understanding
2. Existing Code Analysis
3. Modification Plan
4. Specific Changes
5. Testing and Verification
6. Risks and Precautions
If you are only asked to review code, output:
1. Problem List
2. Severity: Critical / High / Medium / Low
3. Impact Explanation
4. Modification Suggestions
5. Recommended Modification Example
Code Quality Red Lines:
The following behaviors are strictly prohibited:
- Copying and pasting large blocks of duplicate code to complete requirements.
- Stuffing business logic into handlers.
- Passing `map[string]interface{}` everywhere.
- Using global variables to bypass dependency injection.
- Arbitrarily adding util/helper trash-can packages.
- Ignoring errors.
- Catch-all style error handling.
- Continuing to stack logic when functions exceed reasonable length.
- Modifying unrelated code.
- Changing existing behavior without explanation.
- Modifying core logic without tests.
- Introducing large dependencies just to solve small problems.
- Writing code without explaining the verification method.
- Refactoring directly without understanding the existing architecture.
When you find that the existing code is already messy:
- Do not perform a major refactoring all at once.
- Stop the bleeding locally first.
- Write new code within clear boundaries as much as possible.
- Only make necessary changes to old code.
- If refactoring is needed, propose a phased plan first.
Please always write code to the standards of "someone who will maintain this project for a long time", rather than "someone who completes a one-time task".
+55 -20
View File
@@ -2,7 +2,7 @@ import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
export default defineAdditionalConfig({
description:
'OpenFlare is a lightweight, self-hosted OpenResty control plane for reverse proxy rules, releases, node sync, TLS certificates, and basic observability.',
'OpenFlare is a lightweight, self-hosted OpenResty control plane for managing reverse proxy rules, configuration publishing, node synchronization, TLS certificates, and basic observability.',
themeConfig: {
nav: nav(),
@@ -19,9 +19,37 @@ export default defineAdditionalConfig({
},
footer: {
message: 'Released under the Apache License 2.0.',
message: 'Released under the Apache License 2.0',
copyright: 'Copyright © OpenFlare contributors'
}
},
docFooter: {
prev: 'Previous Page',
next: 'Next Page'
},
outline: {
label: 'On this page'
},
lastUpdated: {
text: 'Last updated at'
},
notFound: {
title: 'Page Not Found',
quote: 'This document does not have a corresponding page yet.',
linkLabel: 'Go to Home',
linkText: 'Back to OpenFlare Docs'
},
langMenuLabel: 'Language',
returnToTopLabel: 'Back to top',
sidebarMenuLabel: 'Menu',
darkModeSwitchLabel: 'Theme',
lightModeSwitchTitle: 'Switch to light theme',
darkModeSwitchTitle: 'Switch to dark theme',
skipToContentLabel: 'Skip to content'
}
})
@@ -40,15 +68,14 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
items: [
{ text: 'Overview', link: '' },
{ text: 'Quick Start', link: 'quick-start' },
{ text: 'Usage', link: 'usage' },
{ text: 'Deployment', link: 'deployment' },
{ text: 'SSO Login', link: 'sso' },
{ text: 'Run Server', link: 'server' },
{ text: 'Connect Agent', link: 'agent' },
{ text: 'Publish First Site', link: 'first-site' },
{ text: 'Upgrade and Maintenance', link: 'upgrade' },
{ text: 'Local Development', link: 'development' },
{ text: 'Troubleshooting', link: 'troubleshooting' }
{ text: 'Basic Usage', link: 'usage' },
{ text: 'Tunnel & Intranet Penetration', link: 'tunnel-usage' },
{ text: 'WAF Security Protection', link: 'waf-usage' },
{ text: 'WAF Auto IP Group Expressions', link: 'waf-ip-group-expr' },
{ text: 'SSO Login Configuration', link: 'sso' },
{ text: 'Publish First Configuration', link: 'first-site' },
{ text: 'Troubleshooting', link: 'troubleshooting' },
{ text: 'Credits', link: 'credits' }
]
}
]
@@ -60,10 +87,16 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
text: 'Reference',
items: [
{ text: 'Overview', link: '' },
{ text: 'Configuration', link: 'configuration' },
{ text: 'Commands and Scripts', link: 'cli' },
{ text: 'API Conventions', link: 'api' },
{ text: 'Repository Layout', link: 'repository' }
{ text: 'System Architecture', link: '../design/architecture' },
{ text: 'Launch Server', link: '../deployment/server' },
{ text: 'Access Agent', link: '../deployment/agent' },
{ text: 'Deployment Guide', link: '../deployment/deployment' },
{ text: 'Deploy Relay (Tunnel)', link: '../deployment/relay' },
{ text: 'Deploy OpenFlared', link: '../deployment/openflared' },
{ text: 'Upgrade & Maintenance', link: '../deployment/upgrade' },
{ text: 'Configuration Options', link: 'configuration' },
{ text: 'CLI Commands', link: 'cli' },
{ text: 'API Conventions', link: 'api' }
]
}
]
@@ -74,10 +107,12 @@ function sidebarDesign(): DefaultTheme.SidebarItem[] {
{
text: 'Design',
items: [
{ text: 'Product Boundary', link: '' },
{ text: 'Architecture', link: 'architecture' },
{ text: 'Release Model', link: 'release-model' },
{ text: 'Development Constraints', link: 'development' }
{ text: 'Product Boundaries', link: '' },
{ text: 'System Architecture', link: 'architecture' },
{ text: 'Agent & Publish Model', link: 'agent-design' },
{ text: 'Tunnel & Intranet Penetration', link: 'tunnel-design' },
{ text: 'WAF Design', link: 'waf-design' },
{ text: 'Repository Structure', link: 'repository' }
]
}
]
+176
View File
@@ -0,0 +1,176 @@
# Access Agent
You will learn: The responsibilities of the Agent, the difference between the two access Tokens, installation script parameters, `agent.json` settings, and how to verify that the node has successfully connected.
The OpenFlare Agent runs on the proxy node. It does not receive arbitrary remote shell commands; instead, it pulls the configuration version published by the control plane via the Agent API, writes files for OpenResty locally, executes configuration validation, reloads, and attempts to roll back to a working configuration if it fails.
## Connection Credentials
| Method | Applicable Scenario |
| --- | --- |
| `discovery_token` | Automatically registers a node for the first time, which the Server exchanges for a node-specific credential |
| `agent_token` | Node has already been created/allocated in the management console, directly uses this node-specific credential |
At least one of `agent_token` or `discovery_token` must be configured.
### Credential Retrieval Path
- **`discovery_token` (Auto Registration Token)**: Log into the management console, navigate to "System Settings" -> "Auto Registration", where you can generate, view, and copy the global auto-registration credential.
- **`agent_token` (Node Specific Token)**: Log into the management console, navigate to "Node Management" -> "Add Node", fill in basic node information, save, and copy the node-specific access Token in the node details.
## One-Click Installation
Using the `discovery_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--discovery-token YOUR_DISCOVERY_TOKEN
```
Using the node-specific `agent_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--agent-token YOUR_AGENT_TOKEN
```
The installation script downloads the latest Agent, writes to `/opt/openflare-agent` by default, generates `agent.json`, and registers `openflare-agent.service` on Linux + systemd environments.
Supported arguments:
| Argument | Description | Default Value |
| --- | --- | --- |
| `--server-url` | Server address (required) | |
| `--discovery-token` | One-time auto-registration Token | |
| `--agent-token` | Node-specific Token | |
| `--install-dir` | Target installation directory | `/opt/openflare-agent` |
| `--openresty-path` | Path to the OpenResty binary; automatically detects `openresty` if unspecified | |
| `--repo` | GitHub repository to download from | `Rain-kl/OpenFlare` |
| `--no-service` | Do not register systemd service | |
## Configuration File
Default configuration file path:
```text
/opt/openflare-agent/agent.json
```
Example local configuration:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token",
"data_dir": "./data",
"openresty_path": "openresty",
"openresty_observability_port": 18081,
"observability_replay_minutes": 15,
"heartbeat_interval": 10000,
"request_timeout": 10000
}
```
Example customized OpenResty paths configuration:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token",
"data_dir": "/var/lib/openflare-agent",
"openresty_path": "/usr/local/openresty/nginx/sbin/openresty",
"main_config_path": "/var/lib/openflare-agent/etc/nginx/nginx.conf",
"route_config_path": "/var/lib/openflare-agent/etc/nginx/conf.d/openflare_routes.conf",
"access_log_path": "/var/lib/openflare-agent/var/log/openflare/access.log",
"cert_dir": "/var/lib/openflare-agent/etc/nginx/certs",
"lua_dir": "/var/lib/openflare-agent/etc/nginx/lua",
"runtime_config_dir": "/var/lib/openflare-agent/etc/openflare",
"heartbeat_interval": 10000,
"request_timeout": 10000
}
```
If `openresty_path` is not configured, the Agent calls `openresty` by default. For the full fields, see [Configurations Reference](../reference/configuration.md#agent-configurations-fields).
## Running in Docker
For Docker deployments, run the Agent image containing built-in OpenResty directly:
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
```
## Start & Validate
In a systemd environment:
```bash
systemctl start openflare-agent
systemctl status openflare-agent
journalctl -u openflare-agent -f
```
Manual execution:
```bash
/opt/openflare-agent/openflare-agent -config /opt/openflare-agent/agent.json
```
Running from source:
```bash
cd openflare_agent
export LOG_LEVEL='info'
go run ./cmd/agent -config /path/to/agent.json
```
Running compiled binary:
```bash
cd openflare_agent
go build -o openflare-agent ./cmd/agent
export LOG_LEVEL='info'
./openflare-agent -config /path/to/agent.json
```
Confirm in the management console:
| Position | Expected Result |
| --- | --- |
| Node List | Node status is online |
| Node Details | Heartbeat, current version, and basic resource metrics display correctly |
| Apply Logs | Application result displays after publishing |
## Uninstall
To completely uninstall the Agent and wipe local data:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/uninstall-agent.sh | bash
```
Supported arguments:
| Argument | Description | Default Value |
| --- | --- | --- |
| `--install-dir` | Installation directory | `/opt/openflare-agent` |
| `--service-name` | systemd service name | `openflare-agent` |
The uninstallation script only removes the Agent service, processes, and installation directory; it does not uninstall OpenResty from the host.
## Common Questions
| Symptom | Actions |
| --- | --- |
| `agent_token and discovery_token cannot both be empty` | Check if at least one Token is configured in `agent.json` |
| Node stays offline | Run `curl -I http://your-server:3000` on the Agent node to verify that the Server is reachable |
| OpenResty is not running | Review `journalctl -u openflare-agent`, checking that `openresty_path` is executable and ports 80/443 are not bound |
| Repeated application failures after publishing | The Agent blocks repeated sync attempts of the same failing `version + checksum`; fix the configuration and republish, or activate an older version to roll back |
+287
View File
@@ -0,0 +1,287 @@
# Deployment Guide
You will learn: The recommended deployment strategies for OpenFlare, the system requirements for Server and Agent, how to run from source, integration steps, upgrades, and uninstallation entrypoints.
In production environments, we highly recommend using PostgreSQL as the Server database and explicitly configuring `SESSION_SECRET` for the Server. The recommended Agent deployment method is Docker (which runs the Agent image containing built-in OpenResty); host systemd service installation via script and manual local run are also supported.
## Deployment Topology
### Standard Reverse Proxy Traffic Path
```text
Browser
|
v
OpenFlare Server :3000
|
| Agent API / heartbeat / config pull
v
OpenFlare Agent
|
v
OpenResty binary
|
v
Origin service
```
### Intranet Penetration Traffic Path
```text
Browser
|
v
OpenResty (Agent, WAF/HTTPS Termination) <-- TunnelRelay Node
|
| proxy_pass (127.0.0.1:{vhost_port})
v
OpenFlareRelay (frps process) <-- TunnelRelay Node
|
| frp tunnel protocol
v
OpenFlared (frpc client) <-- Intranet Server
|
v
Internal Service (192.168.x.x)
```
## Prerequisites
Server:
| Item | Requirement |
| --- | --- |
| Go | `1.25+`, required only when running from source |
| Node.js | `18+`, required only when building the admin frontend from source |
| Database | Writable SQLite parent directory, or a reachable PostgreSQL instance |
| Port | Listens on port `3000` by default |
Agent:
| Item | Requirement |
| --- | --- |
| System | The installation script supports Linux and macOS; the systemd service is created only on Linux + systemd environments |
| Architecture | `amd64` or `arm64` |
| OpenResty | Required to have the `openresty` executable when deploying locally, or specify its path via `--openresty-path` |
| Docker | Required only when deploying the Agent via Docker image |
| Network | The Agent node must be able to reach the Server address |
| GeoIP | WAF regional rules rely on the Agent's local MaxMind mmdb; the Agent initializes a built-in library on startup and updates it periodically |
### Hardware Allocation Recommendations
| Component | Minimum Allocation | Recommended Allocation | Note |
| --- | --- | --- | --- |
| **Server Control Plane** | 1 Core CPU / 1 GB RAM / 10 GB Disk | 2 Cores CPU / 4 GB RAM / 50 GB+ Disk | Expand disk allocation according to log retention windows and concurrency. |
| **Agent Data Plane** | 1 Core CPU / 512 MB RAM / 2 GB Disk | 2 Cores CPU / 2 GB RAM / 10 GB+ Disk | Expand according to concurrent reverse proxy connections and WAF workloads. |
| **Relay Node** | 1 Core CPU / 1 GB RAM / 5 GB Disk | 2 Cores CPU / 2 GB RAM / 20 GB Disk | frps throughput is primarily bounded by CPU processing capacity and bandwidth. |
| **OpenFlared Client** | 1 Core CPU / 256 MB RAM / 1 GB Disk | 1 Core CPU / 512 MB RAM / 5 GB Disk | Runs inside the intranet; utilizes minimal CPU/RAM, optimize for network throughput. |
## Docker Compose Deployment for Server
Create a `docker-compose.yml` file:
```yaml
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: openflare
POSTGRES_USER: openflare
POSTGRES_PASSWORD: replace-with-strong-password
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openflare -d openflare"]
interval: 10s
timeout: 5s
retries: 5
openflare:
image: ghcr.io/rain-kl/openflare:latest
container_name: openflare
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "3000:3000"
environment:
SESSION_SECRET: replace-with-a-long-random-string
DSN: postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable
GIN_MODE: release
LOG_LEVEL: info
volumes:
- openflare-data:/data
volumes:
postgres-data:
openflare-data:
```
Start the Server:
```bash
docker compose up -d
docker compose ps
docker compose logs -f openflare
```
Access `http://localhost:3000` for the first time, using the default credentials `root` / `123456`. Please change the default password immediately after logging in.
## Start Server from Source
First, build the admin frontend:
```bash
cd openflare_server/web
corepack enable
pnpm install
pnpm build
```
Then, launch the Server:
```bash
cd openflare_server
export SESSION_SECRET='replace-with-a-long-random-string'
export SQLITE_PATH='./openflare.db'
export LOG_LEVEL='info'
# Optional: Prefer PostgreSQL by setting DSN
# export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable'
go run .
```
By default, the Server listens on port `3000`. You can also specify it explicitly:
```bash
go run . --port 3000 --log-dir ./logs
```
## Running Agent in Docker (Recommended)
Docker is the recommended deployment method for the Agent. Running the Agent image directly launches the Agent controller alongside the built-in OpenResty binary. If `node_ip` is left blank, the Agent automatically resolves its outbound public IP via third-party APIs, avoiding registering the Docker bridge address as the node IP.
Mounting the configuration file:
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-v openflare-agent-data:/data \
-v ./agent.json:/etc/openflare/agent.json:ro \
ghcr.io/rain-kl/openflare-agent:latest
```
Using environment variables:
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
```
## Agent Connection via Installation Script
Apart from Docker, you can deploy the Agent directly on a Linux/macOS host using the installation script.
Auto-register using `discovery_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--discovery-token YOUR_DISCOVERY_TOKEN
```
Connect using node-specific `agent_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--agent-token YOUR_AGENT_TOKEN
```
Installation script arguments:
| Argument | Description | Default Value |
| --- | --- | --- |
| `--server-url` | Server address (required) | |
| `--discovery-token` | Auto-registration Token; mutually exclusive with `--agent-token` | |
| `--agent-token` | Node-specific Token; mutually exclusive with `--discovery-token` | |
| `--install-dir` | Target installation directory | `/opt/openflare-agent` |
| `--openresty-path` | Path to the OpenResty binary; automatically detects `openresty` if unspecified | |
| `--repo` | GitHub repository to download from | `Rain-kl/OpenFlare` |
| `--no-service` | Do not register systemd service | |
Confirm service status:
```bash
systemctl status openflare-agent
journalctl -u openflare-agent -f
```
## Running the Agent Manually
Running from source:
```bash
cd openflare_agent
export LOG_LEVEL='info'
go run ./cmd/agent -config /path/to/agent.json
```
Running compiled binary:
```bash
cd openflare_agent
go build -o openflare-agent ./cmd/agent
export LOG_LEVEL='info'
./openflare-agent -config /path/to/agent.json
```
Minimal `agent.json` example:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token",
"data_dir": "./data",
"openresty_path": "openresty",
"heartbeat_interval": 10000,
"request_timeout": 10000
}
```
If `openresty_path` is left blank, the Agent calls `openresty` by default.
By default, the Agent attempts to upgrade the HTTP heartbeat connection to WebSocket once successfully registered. Once upgraded, configuration activations on the Server notify the Agent instantly; if WebSocket disconnects or fails to establish, the Agent gracefully falls back to HTTP polling.
WAF geographical filtering depends on the local `GeoLite2-Country.mmdb`. The Agent automatically writes the built-in database to `data_dir/etc/openflare/GeoLite2-Country.mmdb` on startup and checks for periodic updates. Muted warnings are logged if updates fail, having no impact on Nginx configuration sync or reloads.
## Upgrades & Uninstallation
Server:
* Root users can check and trigger Server upgrades in the top header of the management console.
* To deploy preview releases, manually check the GitHub Releases page.
* You can also trigger upgrades by uploading the compiled Server binary in the console.
Agent:
* By default, the Agent automatically upgrades following stable releases.
* Agent self-updates require the GitHub Release to contain the compiled binary and a matching `.sha256` checksum file; updates are blocked if the downloaded binary fails the SHA-256 validation.
* You can re-execute the installation script to redeploy or force-update the Agent.
* Upgrading to preview releases requires a manual trigger.
Uninstalling the Agent:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/uninstall-agent.sh | bash
```
The uninstallation script stops the Agent process, removes the systemd service unit, and wipes the installation directory, without uninstalling OpenResty from the host.
+24
View File
@@ -0,0 +1,24 @@
# Deployment & Upgrade
This section provides detailed deployment guides, configuration instructions, and upgrade maintenance procedures for the OpenFlare Server, Agent, Relay, and the OpenFlared client.
## Content Navigation
### Quick Start
* **[Quick Start](../guide/quick-start.md)**: Start the Server and your first Agent in under 5 minutes using Docker Compose (recommended for new users).
### Server Deployment
* **[Launch Server](./server.md)**: Learn how to build the frontend from source, start the Server, and choose between SQLite or PostgreSQL.
### Agent Deployment
* **[Deploy Agent](./agent.md)**: Explore Agent connection methods, Docker deployment, host script installation, config files, and troubleshooting.
### Tunnel Intranet Penetration Deployment
* **[Deploy Relay](./relay.md)**: View config descriptions, Docker deployment, and host runtime guides for TunnelRelay nodes.
* **[Deploy OpenFlared](./openflared.md)**: Access config descriptions, Docker runtime, and auto-sync mechanisms for the intranet client.
### Upgrade & Maintenance
* **[Upgrade & Maintenance](./upgrade.md)**: Discover upgrading procedures for Server/Agent, data retention rules, and validation commands.
### Reference Manuals
* **[Deployment Guide](./deployment.md)**: Browse deployment topologies, prerequisites, Docker Compose samples, and multiple deployment strategies.
+119
View File
@@ -0,0 +1,119 @@
# Deploy OpenFlared Client
You will learn: The responsibilities of the OpenFlared client, configuration parameters and environment variables, how to run the client via Docker, and how to deploy the client on an intranet server using the compiled host binary.
**OpenFlared** is a tunnel client deployed in the user's intranet environment (LANs, private VPCs, or other environments that cannot be directly accessed from the public internet). Its core responsibility is to establish communication with the control plane (OpenFlare Server) via the `X-Tunnel-Token` header, automatically spawning and managing one or more **frpc (Fast Reverse Proxy Client)** subprocesses locally to securely and stably tunnel HTTP traffic back to public relay nodes.
---
## Prerequisites
1. **Retrieve Tunnel Token**: Create a new tunnel instance on the "Intranet Penetration" or "Tunnel Management" page in the OpenFlare management console; the system will automatically generate a unique `tunnel_id` and a `tunnel_token` (e.g., `tun-<32hex>`).
2. **Outbound Network Permissions**: The intranet server does not require any inbound public IPs or port mappings, but it must be able to reach the **OpenFlare Server URL** and the corresponding **TunnelRelay node control port (default 7000)** over the outbound network.
3. **Software Dependencies** (Host deployment only):
- You must have an executable `frpc` binary locally (recommended version `v0.61.0+` or the latest stable `v0.69.0`), or specify its path explicitly in the configuration.
---
## Configuration & Environment Variables
`openflared` reads `flared.json` in the working directory by default on startup. Overriding options via environment variables is fully supported.
### Configuration Fields Details
| JSON Field | Environment Variable | Description | Default Value |
| --- | --- | --- | --- |
| `server_url` | `OPENFLARE_SERVER_URL` | OpenFlare Server API base URL | **None (Required)** |
| `tunnel_token` | `OPENFLARE_TUNNEL_TOKEN` | Tunnel client dedicated access Token | **None (Required)** |
| `frpc_path` | `OPENFLARE_FRPC_PATH` | Path to the `frpc` executable binary | `"frpc"` |
| `data_dir` | `OPENFLARE_DATA_DIR` | Directory to store local data and generated `frpc_{relayNodeID}.toml` configs | `"./data"` |
| `state_path` | - | Path to store local state JSON file (saving the last applied version) | `"{data_dir}/flared-state.json"` |
| `heartbeat_interval`| - | Heartbeat reporting interval (ms or Go Duration string) | `10000` (10s) |
| `sync_interval` | - | Tunnel config polling interval (ms or Go Duration string) | `30000` (30s) |
| `request_timeout` | - | HTTP request timeout duration | `10000` (10s) |
---
## Docker Deployment (Recommended)
Docker is the simplest and safest way to run the client inside the intranet. The official `openflared` image embeds the client controller and `frpc v0.69.0` out of the box, requiring no environment setup.
```bash
docker pull ghcr.io/rain-kl/openflared:latest
docker rm -f openflared 2>/dev/null || true
docker run -d --name openflared --restart unless-stopped \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_TUNNEL_TOKEN=YOUR_TUNNEL_TOKEN \
-v openflared-data:/app/data \
ghcr.io/rain-kl/openflared:latest
```
---
## Manual Host Deployment
If you need to run the client directly on a Linux/macOS/Windows host inside the intranet:
### 1. Compile the Binary
```bash
cd openflared
go build -o flared ./cmd/flared
```
### 2. Prepare `flared.json`
Create a `flared.json` configuration file in the same directory as the executable:
```json
{
"server_url": "http://your-server-ip:3000",
"tunnel_token": "your-tunnel-auth-token",
"frpc_path": "/usr/local/bin/frpc",
"data_dir": "./data",
"heartbeat_interval": "10s",
"sync_interval": "30s"
}
```
### 3. Start the Service
```bash
export LOG_LEVEL='info'
./flared -config ./flared.json
```
---
## Start & Validate
### 1. Auto-Sync Workflow
Once started successfully, OpenFlared operates the following workflow:
- **Heartbeat & Config Fetching**: Periodically polls `/api/flared/heartbeat` and `/api/flared/config` endpoints to validate the Token and evaluate configuration versions.
- **File Rendering**: When a new configuration version (or checksum mismatch) is detected, it pulls the complete tunnel routing rules. If multiple Relays are bound, it renders `frpc_{relayNodeID}.toml` configurations in `data_dir` for each Relay.
- **Hot Reload or Restart**: Spawns the corresponding `frpc` subprocesses, or executes `frpc reload` / restart actions when configurations change, ensuring traffic mappings are kept up to date.
- **Process Auto-Recovery**: If a local `frpc` tunnel process exits unexpectedly, the master program automatically restarts it after a 5-second backoff penalty.
### 2. View Logs & Connection Status
```bash
# Docker container logs
docker logs -f openflared
```
If running correctly, the logs will show output similar to:
```text
flared config loaded ...
detected frpc version v0.69.0
flared process started
applying new tunnel config {"version": "...", "checksum": "..."}
frpc process missing, starting {"relay_id": "..."}
```
### 3. Verify in the Management Console
Open the **"Intranet Penetration"** page in the management console:
- Check the online status of the corresponding tunnel; it should display green as **"Online"**.
- You can inspect which relay nodes the tunnel is connected to, and view the detailed routing configurations of the intranet services.
+126
View File
@@ -0,0 +1,126 @@
# Deploy Relay (Tunnel Relay)
You will learn: The responsibilities of a TunnelRelay node, `openflare_relay` configuration parameters and environment variables, how to run the Relay via Docker, and how to build and deploy the Relay from source manually.
In the OpenFlare intranet penetration architecture, the **TunnelRelay node** plays a key role. Unlike standard Edge Nodes, in addition to running the traditional Agent (managing OpenResty for HTTPS/WAF processing), it co-locates the **Relay (frps tunnel manager)** service, responsible for listening to intranet client (OpenFlared) tunnel connections and relaying traffic.
---
## Prerequisites
Before deploying a TunnelRelay node, ensure:
1. **Registered as a TunnelRelay node**: Add a node of type `tunnel_relay` in the OpenFlare management console under "Node Management", and retrieve its node-specific `agent_token` or use the global `discovery_token`.
2. **Network Ports**:
- Ensure `bindPort` (the port frpc clients connect to, default `7000`) is accessible from the public/intranet client networks.
- Ensure `vhostHTTPPort` (the HTTP Vhost port, default `8080`) is free and not bound by other processes, as the Agent routes traffic to frps on this port.
3. **Software Dependencies** (Host deployment only):
- You must have an executable `frps` binary locally (recommended version `v0.61.0+` or the latest stable `v0.69.0`), or specify its path explicitly in the configuration.
---
## Configuration & Environment Variables
`openflare_relay` reads `relay.json` in the working directory by default on startup. Overriding options via environment variables is fully supported.
### Configuration Fields Details
| JSON Field | Environment Variable | Description | Default Value |
| --- | --- | --- | --- |
| `server_url` | `OPENFLARE_SERVER_URL` | OpenFlare Server API base URL | **None (Required)** |
| `agent_token` | `OPENFLARE_AGENT_TOKEN` | Node-specific Token | Mutually exclusive with below |
| `discovery_token` | `OPENFLARE_DISCOVERY_TOKEN` | One-time auto-registration Token | Mutually exclusive with above |
| `node_name` | `OPENFLARE_NODE_NAME` | Custom name for the node | Hostname by default |
| `node_ip` | `OPENFLARE_NODE_IP` | Outbound/listening IP of the node | Automatically detects real outbound IP |
| `frps_path` | `OPENFLARE_FRPS_PATH` | Path to the `frps` executable binary | `"frps"` |
| `data_dir` | `OPENFLARE_DATA_DIR` | Directory to store local data and generated `frps.toml` | `"./data"` |
| `state_path` | - | Path to store local state JSON file | `"{data_dir}/relay-state.json"` |
| `heartbeat_interval`| - | Heartbeat interval (integer ms or Go Duration string) | `10000` (10s) |
| `request_timeout` | - | HTTP request timeout duration | `10000` (10s) |
---
## Docker Deployment (Recommended)
Docker is the most convenient way to deploy a TunnelRelay node. The official Docker image embeds the `openflare-relay` controller and `frps v0.69.0` out of the box.
```bash
docker pull ghcr.io/rain-kl/openflare-relay:latest
docker rm -f openflare-relay 2>/dev/null || true
docker run -d --name openflare-relay --restart unless-stopped \
-p 7000:7000 \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
-v openflare-relay-data:/var/lib/openflare-relay \
ghcr.io/rain-kl/openflare-relay:latest
```
> [!TIP]
> The `-p 7000:7000` option maps the port `frpc` clients connect to. If a custom `relay_bind_port` is configured in the management console, change this port mapping on the host accordingly.
---
## Manual Host Deployment
If you prefer to run the Relay directly on a physical host or VM:
### 1. Compile the Binary
```bash
cd openflare_relay
go build -o openflare-relay ./cmd/relay
```
### 2. Prepare `relay.json`
Create a `relay.json` configuration file in the same directory as the executable:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "your-relay-node-agent-token",
"frps_path": "/usr/local/bin/frps",
"data_dir": "./data",
"heartbeat_interval": "10s",
"request_timeout": "10s"
}
```
### 3. Start the Service
```bash
export LOG_LEVEL='info'
./openflare-relay -config ./relay.json
```
---
## Start & Validate
### 1. View Process Logs
```bash
# Docker container logs
docker logs -f openflare-relay
```
If managed via systemd on Linux, execute:
```bash
journalctl -u openflare-relay -f
```
### 2. Verify Runtime Status
Upon starting successfully, the Relay operates as follows:
- Sends HTTP heartbeats to register and go online with the control plane.
- Retrieves the active frps baseline settings (including `bindPort`, `vhostHTTPPort`, and the auto-generated `auth_token`).
- Automatically renders the `data/frps.toml` configuration locally.
- Spawns the subprocess `frps -c data/frps.toml`.
- If the `frps` process crashes, the Relay automatically restarts it after 2 seconds.
### 3. Verify in the Management Console
Log into the management console and navigate to **"Node Management"** to verify:
- The TunnelRelay node status is marked as **"Online"**.
- The Node Type is correctly displayed as **Relay Node** and the frps status displays as **Healthy**.
+170
View File
@@ -0,0 +1,170 @@
# Launch Server
You will learn: How to build the admin frontend from source, start the OpenFlare Server, choose between SQLite or PostgreSQL, and access Swagger.
OpenFlare Server is a Gin + GORM monolithic control plane, responsible for managing the Admin UI, Admin API, Agent API, configuration rendering, version publishing, data storage, and aggregated queries.
## Prerequisites
| Item | Requirement |
| --- | --- |
| Go | `1.25+` |
| Node.js | `18+` |
| pnpm | Recommended enabling via `corepack enable` |
| Database | SQLite parent directory must be writable, or a reachable PostgreSQL instance |
In production environments, we highly recommend explicitly configuring `SESSION_SECRET` and prioritizing PostgreSQL.
## Build the Admin Frontend
The Go Server hosts static assets located in `openflare_server/web/build`. Before starting the Server from source, build the frontend:
```bash
cd openflare_server/web
corepack enable
pnpm install
pnpm build
```
Common frontend quality checks:
```bash
pnpm lint
pnpm typecheck
pnpm test
```
## Start with SQLite
```bash
cd openflare_server
export SESSION_SECRET='replace-with-a-long-random-string'
export SQLITE_PATH='./openflare.db'
export LOG_LEVEL='info'
go run .
```
By default, the Server listens on port `3000`. Access it at:
```text
http://localhost:3000
```
## Start with PostgreSQL
```bash
cd openflare_server
export SESSION_SECRET='replace-with-a-long-random-string'
export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable'
export LOG_LEVEL='info'
go run .
```
If `DSN` is set, it takes precedence over SQLite. When both `DSN` and the legacy `SQL_DSN` exist, `DSN` is prioritized.
If the target PostgreSQL database is empty and a local SQLite database exists at `SQLITE_PATH`, the Server automatically migrates the SQLite data into PostgreSQL during startup, outputting the migration progress in the logs.
## Start with Docker
Deploying with Docker avoids the hassle of setting up local Go and Node.js environments. OpenFlare provides official Dockerfiles and Compose configurations to support independent container startups and multi-service orchestrations.
### 1. Quick Start via Docker Run (SQLite Example)
Ensure that a local directory for persisting databases and logs has been created. Run the following command to start the Server:
```bash
# Create local mount directory
mkdir -p ./openflare-data
# Start the container
docker run -d \
--name openflare-server \
-p 3000:3000 \
-v $(pwd)/openflare-data:/data \
-e SESSION_SECRET='replace-with-a-long-random-string' \
-e SQLITE_PATH='/data/openflare.db' \
-e GIN_MODE='release' \
-e LOG_LEVEL='info' \
ghcr.io/rain-kl/openflare:latest
```
Startup parameters:
* **`-p 3000:3000`**: Maps port `3000` on the host to port `3000` inside the container.
* **`-v $(pwd)/openflare-data:/data`**: Mounts the local directory to `/data` in the container, ensuring that the SQLite database `openflare.db` is not lost when restarting or rebuilding the container.
* **`SESSION_SECRET`**: The session signing hash key (required).
---
### 2. One-click Startup via Docker Compose (Integrated PostgreSQL)
We recommend using Docker Compose in production environments to orchestrate an independent PostgreSQL database and establish high-availability relationships.
Create a `docker-compose.yml` file:
```yaml
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: openflare
POSTGRES_USER: openflare
POSTGRES_PASSWORD: replace-with-strong-password
volumes:
- ./postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openflare -d openflare"]
interval: 10s
timeout: 5s
retries: 5
openflare:
image: ghcr.io/rain-kl/openflare:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "3000:3000"
environment:
SESSION_SECRET: replace-with-random-string
SQLITE_PATH: /data/openflare.db
DSN: postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable
GIN_MODE: release
LOG_LEVEL: info
volumes:
- ./openflare-data:/data
```
Start the services:
```bash
docker compose up -d
```
Compose configuration options:
* **`depends_on` and `healthcheck`**: Uses PostgreSQL's health check (`pg_isready`) to ensure that the database is fully initialized and ready before launching the OpenFlare Server, preventing panics from failed database connection attempts on first launch.
* **Separated Data Volume Mounts**: PostgreSQL data is mounted under `./postgres-data`, and OpenFlare data and backups are mounted under `./openflare-data`, making backups and maintenance simple.
## CLI Arguments
```bash
go run . --port 3000 --log-dir ./logs
```
| Argument | Description | Default Value |
| --- | --- | --- |
| `--port` | The port the Server listens to | `3000` |
| `--log-dir` | The directory to write logs to | Empty, outputs to stdout |
| `--version` | Outputs version and exits | `false` |
| `--help` | Outputs help and exits | `false` |
## First Login
Default credentials:
| Username | Password |
| --- | --- |
| `root` | `123456` |
Please change the default password immediately after your first login.
+52
View File
@@ -0,0 +1,52 @@
# Upgrade & Maintenance
You will learn: How to upgrade the Server and the Agent, how to clean up observability data, and which validation commands to execute before and after maintenance.
Before upgrading, verify the currently active version, the most recent Agent application results, and your database backup strategy. In production environments, never trigger upgrades while a configuration is being published, during large-scale Agent reconnections, or while database migrations are in progress.
## Server Upgrade
Root users can check and trigger stable Server upgrades in the top header of the management console. You can also trigger upgrades by uploading the compiled Server binary in the console.
To deploy preview releases, manually check the GitHub Releases page. We highly recommend prioritizing stable releases in production environments.
Verify after upgrading:
```bash
docker compose ps
docker compose logs -n 100 openflare
```
If deployed from source, restart the Server and verify that no database migration or startup errors appear in the logs.
## Agent Upgrade
Node Agents automatically update following stable releases by default. Upgrading to preview releases requires a manual trigger.
You can re-execute the installation script to redeploy or force-update the Agent:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--agent-token YOUR_AGENT_TOKEN
```
Note: Re-executing the current installation script wipes the entire installation directory, including the existing `agent.json`, local states, cached databases, and downloaded binaries. Ensure you have the node Token handy before executing the script.
Verify after upgrading:
```bash
systemctl status openflare-agent
journalctl -u openflare-agent -n 100 --no-pager
```
## Data Maintenance
The management console's Settings page maintains options for automatic cleanup of observability data:
| Parameter | Description |
| --- | --- |
| `DatabaseAutoCleanupEnabled` | Toggles daily automatic cleanup |
| `DatabaseAutoCleanupRetentionDays` | Data retention duration in days, minimum 1 day |
When enabled, the Server cleans up access logs, metrics snapshots, and request reports at 3:00 AM daily.
+181
View File
@@ -0,0 +1,181 @@
# Agent Design Document
You will learn: Agent design principles, core functional modules, interaction links with the Server, and how configuration applications are secured and made reliable through immutable version models and the three-stage disaster recovery rollback mechanism.
---
## Requirements Analysis
In distributed reverse proxy and edge security gateway scenarios, the Agent plays a central role in connecting the control plane (Server) and the data plane (OpenResty). Since the Agent runs on the user's actual node server, its design must adhere to the following core security and high-availability requirements:
1. **Active Pull (Pull Model) instead of Push**: The Server does not hold the SSH keys of the nodes, nor does it actively initiate inbound connections to the nodes. All control directives and configuration updates are actively pulled by the Agent via heartbeats or long-lived connections (WebSockets). This eliminates inbound firewall security risks on the node side and prevents control channels from being hijacked.
2. **Minimal Intrusiveness**: The Agent runs as an independent Go binary process. It only interacts with the local OpenResty process through file-based configuration rewriting and signal notifications, without interfering with other system services on the node.
3. **Robust Disaster Recovery & Self-Healing**: Since network jitter, disk exhaustion, or erroneous configurations can easily lead to configuration sync failures, the Agent must possess zero-dependency local rollback and self-healing capabilities, strictly preventing a single configuration error from causing a complete node outage.
4. **Pure Data and State Landing**: The Agent is only responsible for executing file generation and control intentions rendered by the Server. It does not carry complex control plane duties like business logic validation or multi-tenant authorization, ensuring the node side remains highly efficient and lightweight.
---
## Core Capabilities
The Agent is composed of the following core sub-modules, cooperating to manage its complete lifecycle:
| Module Name | Directory | Responsibilities |
| :--- | :--- | :--- |
| **Config Sync** | `sync/` | Pulls full configuration packages, writes files, triggers reloads, and records and reports sync statuses. |
| **Heartbeat** | `heartbeat/` | Periodically reports node health and resource metrics to the Server and retrieves the latest active version summary. |
| **WebSocket** | `wsclient/` | Maintains a persistent connection with the Server, providing sub-second real-time configuration pushes and commands. |
| **OpenResty Control** | `nginx/` | Executes Nginx config validation (`openresty -t`), rewrites, graceful reloads (`reload`), and process auto-start. |
| **Local State Store** | `state/` | Persistently records local applied versions, error logs, and buffers unsent observability metrics. |
| **Self-Updater** | `updater/` | Listens to Server self-update commands, securely pulls new binary versions, and completes in-place upgrades. |
| **Observability** | `observability/` | Collects host CPU/memory/disk and Nginx performance metrics, processes access logs, and uploads them. |
| **GeoIP Maintenance** | `geoipdata/` `geoipupdate/` | Maintains and updates the local GeoIP database periodically to support WAF country-level filtering. |
---
## Interaction Flows with Server
The Agent communicates with the control plane through **Token-based Auto-Registration** and a **Dual-channel Heartbeat/WebSocket** system during its lifecycle.
### 1. Auto-Registration Flow
If the Agent starts with an empty `access_token` in its local `agent.json`, but has a `discovery_token` configured, it triggers the auto-registration flow:
1. The Agent sends a registration request to `/api/agent/register`, carrying a local hardware fingerprint, IP, and hostname.
2. After validating the `discovery_token`, the Server generates a unique `NodeID` and a dedicated `AccessToken` (i.e., `agent_token`) in the database and returns them.
3. The Agent writes the dedicated Token to its local configuration file, clears the one-time `discovery_token`, and uses the `AccessToken` for all subsequent authenticated communications.
### 2. Dual-Channel Heartbeat & Sync Mechanism
* **HTTP Polling (Fallback and Detection)**: The Agent sends POST heartbeat packets at configured `heartbeat_interval` intervals by default. It reports health metrics while retrieving the currently active configuration version summary (Version & Checksum).
* **WebSocket Channel (Real-time Communication)**: Upon a successful HTTP heartbeat, the Agent automatically attempts to upgrade the connection to WebSocket (`/api/agent/ws`).
* Once the WS connection is established, heartbeats and metrics reporting shift entirely to the WS pipeline, reducing network overhead.
* When the Server publishes or activates a new version, it broadcasts a notification to the Agent via WS. The Agent triggers the synchronization flow **immediately** upon receiving the change event, achieving sub-second configuration deployment.
* If the WS connection drops due to network issues, the Agent automatically falls back to HTTP polling and uses an exponential backoff algorithm to attempt rebuilding the WS channel.
### 3. Interaction Sequence Diagram
```mermaid
sequenceDiagram
autonumber
participant Agent as OpenFlare Agent
participant OR as Local OpenResty
participant Server as OpenFlare Server
Note over Agent: First Startup (No AccessToken)
Agent->>Server: 1. Auto-registration request (carrying discovery_token)
Server-->>Agent: 2. Issue NodeID & dedicated AccessToken (agent_token)
Note over Agent: Store Token in local configuration file
rect rgb(240, 248, 255)
Note over Agent, Server: HTTP Fallback & WebSocket Upgrade
Agent->>Server: 3. Send HTTP Heartbeat (report system metrics & health)
Server-->>Agent: 4. Return ActiveConfig summary & AgentSettings
Agent->>Server: 5. Initiate WebSocket upgrade request (/api/agent/ws)
Server-->>Agent: 6. Upgrade successful (persistent bi-directional channel)
end
rect rgb(245, 245, 245)
Note over Agent, Server: Real-time Configuration Publication
Note over Server: Administrator clicks publish config in UI
Server->>Agent: 7. Broadcast active config summary via WS (WSMessageTypeActiveConfig)
Agent->>Server: 8. Request full configuration details (carrying target Version/Checksum)
Server-->>Agent: 9. Return complete configuration snapshot (Nginx configs, certs, WAF rules, etc.)
Note over Agent: Backup old files, write new config to local temp path
Agent->>OR: 10. Execute config syntax validation (openresty -t)
OR-->>Agent: 11. Return validation result (OK)
Agent->>OR: 12. Send graceful reload signal (openresty -s reload)
Agent->>Server: 13. Report application success status (Apply Log & ActiveVersion)
end
```
---
## Control of OpenResty
The Agent implements end-to-end closed-loop control of the data plane OpenResty, including configuration rendering, syntax validation, graceful reloading, and exception state capturing:
### 1. Configuration Layout on Disk
Upon successful sync, the Agent writes configuration files to `/etc/nginx/openflare-lua/` (or the configured `LuaDir`) according to a strict physical structure:
* `nginx.conf`: Main configuration file (replaces absolute path placeholders, configures performance parameters, shared dictionaries, and global server blocks).
* `routes.conf`: Route configuration file (generated by the Agent, containing all website server blocks, certificate paths, cache settings, and rate limit directives).
* `certs/`: Certificate storage directory (files named as `{cert_id}.crt` and `{cert_id}.key`).
* `waf/` and `pow/`: Dedicated Lua runtime scripts required for WAF and CC mitigation.
* `waf_config.json` and `waf_ip_groups.json`: Structured rules and IP databases required by the WAF filtering engine.
### 2. Refined Reload Operations
1. **Backup Current Config**: Before writing new files, the Agent copies the existing configuration files to a `.backup` directory, keeping a complete rollback snapshot.
2. **Write and Replace Placeholders**: Writes the pulled templates, automatically replacing absolute path placeholders (e.g., `__OPENFLARE_LUA_DIR__`) with actual local execution paths.
3. **Syntax Validation**: Calls `openresty -t -c <temp_nginx.conf>` to run a strict syntax test.
4. **Graceful Reload**: If validation passes, the Agent moves the files to the official paths and executes `openresty -s reload`. If OpenResty is not running, it launches the process.
5. **Exception Capture**: If validation or reload fails, the Agent intercepts the standard error output (stderr) and extracts the first 2000 characters of the detailed error log.
---
## Publishing & Config Application Model
OpenFlare discards the fragile mechanism of dynamically patching node configurations, instead using an **immutable configuration version publishing model**.
```text
Edit rules -> Preview / View diff -> Publish -> Generate full configuration version -> Activate version -> Agent pulls -> Local application -> Report result
```
### 1. Core Design Principles
* **Complete Publication**: Every publication compiles all enabled proxy routes, certificates, and global/custom WAF rules at once, generating a complete version package with a unique `checksum`.
* **Version Format**: Uses the `YYYYMMDD-NNN` incremental format, ensuring version histories are intuitive and strictly monotonic.
* **Global Single Active Version**: The system supports only one globally `active` configuration version at any given time. Rollbacks do not require reverse patching; they simply transition an older healthy version to the `active` state, and the Agent pulls and applies it.
### 2. Three-Stage Disaster Recovery & Rollback Mechanism
If the Agent fails to apply a configuration (or reload fails), it automatically triggers the following three-stage self-healing pipeline:
```mermaid
graph TD
A[Config Application Failed] --> B[Stage 1: Attempt Local Backup Recovery]
B -- Backup Exists --> C[Write Local Backup Files]
C --> D[Run openresty -t Validation]
D -- Validation OK --> E[Reload Old Configuration]
D -- Validation Failed --> F[Proceed to Stage 2]
B -- No Backup --> F[Stage 2: Write Built-in Safe Fallback Config]
F --> G[Write fallback nginx.conf: Listen on Port 80 Only]
G --> H[Enable stub_status health checks]
G --> I[Return 503 for all other routes & block errors]
G --> J[Attempt to launch OpenResty to maintain basic survival]
J --> K[Proceed to Stage 3]
E --> L[Report Apply Warning]
K --> M[Block Local Repeated Application of Failed Version]
M --> N[Report Apply Error with detailed logs]
```
1. **Stage 1: Local Backup Rollback**
* The Agent attempts to restore the main configuration, routes, and certificates from the `.backup` directory.
* It runs `openresty -t` validation on the restored backup. If successful, it reloads and reports a `Warning` to the Server (Warning: failed to apply new version, automatically rolled back to the previous healthy version).
2. **Stage 2: Built-in Safe Fallback Runtime**
* If no local backup exists (e.g., first deployment failed) or if the rollback validation fails, the Agent activates the ultimate self-healing mechanism: writing a **built-in safe fallback configuration**.
* **Fallback Configuration Specification**:
* Listens only on port `80`, containing no real user reverse proxy routes.
* The `/openflare/stub_status` endpoint returns a healthy response, while all other requests uniformly return a `503 Service Unavailable` status code with the fixed response body `OpenFlare: No Valid Configuration`.
* It attempts to launch OpenResty with this minimal configuration. This keeps the Nginx process alive, preserving underlying health probes and metric endpoints, preventing containers/pods from being repeatedly killed and restarted by orchestration systems, while keeping sensitive routes secure.
3. **Stage 3: Local Configuration Blocking**
* The Agent records the failing configuration's `version + checksum` in its local state store blacklist.
* Until the control plane activates a new configuration (resulting in a changed `checksum`), the Agent's heartbeat blocks repeated synchronization pulls of this erroneous version, preventing nodes from entering an infinite loop of "heartbeat -> pull failing config -> crash rollback".
### 3. WAF IP Group Asynchronous Runtime Synchronization
To prevent highly volatile IP blacklists from triggering frequent full config publications and Nginx reloads (which still incur minor CPU and connection overhead), WAF IP groups are synchronized via an **asynchronous differential sync design**:
* **Static Publication Snapshot**: The `waf_config.json` generated upon publication only contains the group ID reference mapping (i.e., `ip_whitelist_group_ids` / `ip_blacklist_group_ids`) and does not contain the actual list of IP addresses.
* **Heartbeat Differential Check**: The Agent uploads its locally cached IP groups MD5 checksum map in its heartbeat.
* **Differential Delivery**: The Server compares checksums and only delivers missing or modified IP groups, which are written directly to `waf_ip_groups.json` on the node without reload.
* **WebSocket Real-time Push**: When an administrator updates an IP group, or a threat intelligence subscription successfully pulls, or a security rule triggers a temporary block, the Server immediately broadcasts the IP group update package via WebSocket. The Agent receives and applies it instantly **without Nginx reloads**.
---
## Design Constraints
To protect the security boundary of the data and control plane, Agent development must strictly comply with the following engineering constraints:
1. **Zero-Privilege Command Execution**: The Server is strictly prohibited from sending any arbitrary shell commands or scripts to the Agent (such as exec/eval). All system control operations (such as start, stop, reload, update) must be hardcoded inside the Agent binary.
2. **Strict Token Filtering and Prefix Validation**: Agent requests to the Server must be prefixed with `/api/agent/` and must carry the `X-Agent-Token` header for signature or token verification.
3. **Node Autonomy**: The Agent must support complete offline capabilities. During disconnected periods, the local OpenResty must rely on local configuration copies to keep reverse proxy services running normally.
+124 -47
View File
@@ -1,8 +1,10 @@
# System Architecture
You will learn: The overall architecture of OpenFlare, the responsibility boundaries of Server, Agent, OpenResty, and the management console frontend, and the request flow of a configuration release from the management console to take effect on a node.
You will learn: The overall architecture of OpenFlare, the boundaries of responsibilities for Server, Agent, OpenResty, and Admin Frontend, and the request flow of a configuration publication from the admin dashboard to activation on a node.
OpenFlare consists of the Server, the Agent, local OpenResty on each node, and the management console frontend. The Server is the control plane, the Agent is the only controlled landing entry point on the node side, and OpenResty is the actual data plane.
OpenFlare consists of the Server, the Agent, the node-local OpenResty, and the Admin Frontend. The Server is the control plane, the Agent is the only controlled entry point on the node side, and OpenResty serves as the actual data plane. In intranet penetration scenarios, the Relay (frps manager) and OpenFlared (frpc manager) extend the data plane traffic path.
### Standard Reverse Proxy Traffic Path
```text
Browser
@@ -24,75 +26,140 @@ OpenResty binary
Origin
```
### Intranet Penetration Traffic Path
```text
Browser
|
| HTTPS request
v
OpenResty (Agent, TLS/WAF) <-- TunnelRelay Node
|
| proxy_pass http://localhost:vhost_port (Host header preserved)
v
OpenFlareRelay (frps) <-- TunnelRelay Node, co-located with Agent
|
| frp tunnel protocol (HTTP Vhost routing by Host header)
v
OpenFlared (frpc) <-- Intranet Server
|
| HTTP/HTTPS forward
v
Internal Service (192.168.x.x)
```
## Component Responsibilities
| Component | Responsibility |
| --- | --- |
| Server | Management UI, admin APIs, Agent APIs, configuration rendering, version publishing, data storage, and aggregate queries |
| Agent | Registration, heartbeats, synchronization, writing files, configuration validation, reloads, fallback/rollbacks, self-updating, and lightweight data collection |
| OpenResty | Receives real traffic, executes WAF, PoW, authentication, and reverse proxying according to configurations rendered by OpenFlare |
| Frontend | Manages website configurations, WAF, origins, certificates, nodes, versions, users, settings, and observability pages |
| Server | Admin UI, Admin API, Agent/Relay/Client API, configuration rendering, version publishing, data storage, and aggregated queries. |
| Agent | Registration, heartbeats, synchronization, file writing, validation, reload, rollback on failure, self-updating, and light metrics collection. |
| OpenResty | Receives real traffic, executing WAF, PoW, authentication, and reverse proxying according to the configuration rendered by OpenFlare. |
| OpenFlareRelay | Manages the lifecycle of the frps process, providing tunnel relay services and receiving frps configurations via heartbeat. |
| OpenFlared | Manages frpc processes (can be multiple), connecting to the Relay and forwarding traffic to intranet services. |
| Frontend | Manages pages for website configs, WAF, origins, certificates, nodes, tunnels, versions, users, settings, and observability. |
## Server
`openflare_server` is a monolithic control plane:
`openflare_server` is the single-control-plane monolith:
* Gin provides HTTP services.
* Gin provides the HTTP services.
* GORM accesses SQLite or PostgreSQL.
* The existing login system provides management console Sessions.
* Authentication source and external account binding support GitHub OAuth and standard OIDC.
* The Go Server hosts the static build output of `openflare_server/web`.
* The existing login system provides Admin Session management.
* Authentication sources support GitHub OAuth and standard OIDC logins with external account binding.
* The Go Server hosts the `openflare_server/web` static build assets.
The Server does not directly SSH into nodes, nor does it modify node files online. It only saves the control plane state, generates complete configuration versions, and lets nodes actively pull them via the Agent API.
The Server does not directly SSH to nodes, nor does it modify node files online. It only stores control plane state, generates complete configuration versions, and lets nodes actively pull them via the Agent API.
## Agent
`openflare_agent` is a Go monolithic application:
* Runs on nodes as a single binary.
* Reads or generates local node information upon startup.
* Performs periodic heartbeats to report status and fetch the active version summary.
* Pulls configurations, backs up old files, writes new files, validates, and reloads upon discovering a new version.
* Attempts to restore execution and roll back when the application fails.
* Maintains the WAF GeoIP mmdb; writes the built-in initial database on startup and updates it regularly based on configuration.
* Runs as a single binary on the node side.
* Reads or generates local node information on startup.
* Performs periodic heartbeat check-ins to report status and retrieve active version summaries.
* Upon discovering a new version, it pulls the configuration, backs up old files, writes new files, validates them, and reloads.
* Automatically rolls back to restore operations if the application fails.
* Maintains the local WAF GeoIP mmdb, writing the built-in library on startup and updating it periodically based on configuration.
The Agent uniformly executes validations, reloads, starts, and restarts via the OpenResty binary pointed to by `openresty_path`; it falls back to calling `openresty` by default when not configured. In Docker deployments, the Agent image includes the OpenResty binary and follows the same binary control logic.
The Agent executes validation, reload, startup, and restart uniformly via the path specified in `openresty_path`; if unconfigured, it defaults to calling `openresty`. During Docker deployments, the Agent image packages OpenResty and follows the same execution control logic.
Node IPs are maintained by Agent registration and heartbeat reports by default; when the admin UI locks a node IP, the Server continues updating runtime fields such as status, versions, and observability, but no longer accepts Agent reports to overwrite that IP.
The node IP is maintained by default through Agent registration and heartbeat reporting; if the administrator locks the node IP, the Server only updates running status, versions, and observability fields, and no longer accepts reports from the Agent to override the locked IP.
## Frontend
`openflare_server/web` is the official management console frontend:
`openflare_server/web` is the official Next.js-based frontend:
* Next.js App Router.
* Next.js 15 App Router.
* React 19.
* TypeScript.
* Tailwind CSS.
* TanStack Query manages server state.
* TanStack Query for server-side state.
The frontend is hosted by the Go Server after static export. All API requests must go through `lib/api/` uniformly and handle the `success/message/data` response structure.
The frontend uses static export mode (`output: 'export'`), which is then hosted by the Go Server using `embed.FS`. All API requests must go through `lib/api/` and process the `success/message/data` response structure.
## Data and Request Flow
The Server integrates the following security features:
* CORS middleware: Cross-Origin Resource Sharing protection.
* Rate limiting: Global and key API endpoint throttling.
* Session management: Cookie/Redis-based session storage.
### Management Console Request Flow
## Data & Request Flow
### Management Request Flow
```text
Browser -> Frontend -> /api/* -> controller -> service -> model -> database
```
Mutation APIs on the management console use `POST`, while read-only APIs use `GET`. Both success and failure return a clear `message`.
Admin mutation APIs use `POST`, while read-only APIs use `GET`. Both success and failure responses return a clear `message`.
### Agent Sync Flow
```text
Agent heartbeat -> Server returns active version summary
Agent discovers new version -> Pulls configuration details
Agent writes main configuration / route configuration / certificates / Lua resources / WAF runtime configuration
Agent executes OpenResty validation and reload
Agent HTTP heartbeat -> Server returns active version summary
Agent detects new version -> Pulls complete configuration details
Agent writes main configuration / route configurations / certificates / Lua resources / WAF runtimes
Agent runs OpenResty validation (openresty -t) and reload
Agent reports application result
```
When WebSocket (WS) connection upgrade is enabled by default, the Agent first obtains settings through the HTTP heartbeat, and then attempts to connect to the Agent WebSocket. Once the WS connection is successful, periodic status reporting is carried by WS; when the Server publishes or activates a version, it broadcasts the active version summary to connected Agents, allowing them to enter the synchronization flow immediately. When the WS connection is disconnected or fails to establish, the Agent automatically falls back to the HTTP heartbeat.
### Relay Sync Flow
The Relay (OpenFlareRelay process) runs on the TunnelRelay node and shares the same `agent_token` with the Agent:
```text
Relay HTTP heartbeat -> Server returns frps base configuration (bindPort, vhostHTTPPort, auth_token)
Relay generates frps.toml and starts or updates the frps process
Relay periodically reports frps health status and connection statistics
Relay attempts WebSocket upgrade for real-time configuration pushes
```
frps configurations are relatively static (ports, auth token), dispatched via heartbeats, and **not included in the versioned publishing flow**. The Relay must monitor the frps process and auto-recover it on failures. Authentication: `X-Agent-Token` + API path prefix `/api/relay/*`, distinguished by Server via `node_type = tunnel_relay`.
### OpenFlared Sync Flow
OpenFlared (client) runs inside the intranet server, using independent `tunnel_token` authentication:
```text
Client HTTP heartbeat -> Server returns tunnel configuration version summary (version, checksum)
Client detects new version -> Pulls complete tunnel route configuration (relay list + frpc proxy definitions)
Client generates independent frpc.toml configuration files for each Relay
Client starts a new frpc process for new Relays, or hot-reloads (frpc reload) existing ones
Client reports application results (success/failure details)
```
OpenFlared communicates with the Server via `/api/flared/*` using the `X-Tunnel-Token` header. Tunnel route configurations are versioned along with the publishing flow, ensuring all configuration changes are consistently published to both Agents and Clients via a single version number.
**WebSocket Upgrade Flow** (Optional, controlled via `AgentWebsocketUpgradeEnabled`):
When WebSocket upgrade is enabled:
1. The Agent retrieves run configurations and settings via HTTP heartbeat.
2. The Agent attempts to upgrade the connection to `GET /api/agent/ws` (WebSocket).
3. Once the WS connection is established, periodic state reporting and real-time commands are carried over the WebSocket pipeline, minimizing latency.
4. When the Server publishes or activates a version, it immediately broadcasts the active version summary to connected Agents, triggering the sync flow instantly.
5. If the WebSocket disconnects or fails to establish, the Agent automatically falls back to HTTP heartbeats, ensuring high availability.
Through the `OpenRestyWebsocketEnabled` option, WebSocket reverse proxy support can be enabled or disabled at the OpenResty layer.
### Reverse Proxy Flow
@@ -100,18 +167,21 @@ When WebSocket (WS) connection upgrade is enabled by default, the Agent first ob
Client -> OpenResty server block -> WAF Lua -> named upstream -> Origin
```
Website configuration is the aggregation boundary of reverse proxies. A website configuration can bind multiple domains and share site-level traffic limits, reverse proxies, and caching configurations.
Website configurations are the boundaries of reverse proxy aggregation. A single website configuration can bind multiple domains, sharing site-level rate limiting, reverse proxy, and cache settings.
WAF is executed in the OpenResty `access_by_lua_file` phase. Rules come from `waf_config.json` carried in the current active version; the global rule group takes effect by default, and websites can overlay custom rule groups.
WAF executes in the OpenResty `access_by_lua_file` phase. Rules originate from the `waf_config.json` carried in the currently active version; global rule groups take effect by default, and websites can overlay custom rule groups. `waf_config.json` only stores rule group references and IP group IDs; IP group members are synchronized independently by the Agent into `waf_ip_groups.json`, and the OpenResty Lua engine merges and evaluates them by reference ID.
WAF IP groups are managed by the Server. Manual IP groups store IP/CIDR lists directly; auto IP groups are evaluated by Server cron jobs reading request logs and applying Expr boolean rules; subscription IP groups are fetched by Server cron jobs from remote text or JSON sources. The Agent reports local IP group checksums in heartbeats, and the Server only returns mismatched IP groups. When an IP group is updated on the Server, a broadcast is sent via WebSocket to push changes, and the OpenResty Lua reads the local JSON file directly without querying the DB, request logs, or remote subscription sources.
## Core Objects
Currently active entities include:
Current valid entities include:
* `proxy_routes`
* `origins`
* `config_versions`
* `nodes`
* `tunnels`
* `auth_sources`
* `external_accounts`
* `node_system_profiles`
@@ -124,23 +194,30 @@ Currently active entities include:
* `traffic_analytics_rollups`
* `node_health_events`
* `waf_rule_groups`
* `waf_ip_groups`
* `waf_rule_group_bindings`
* `acme_accounts`
* `dns_accounts`
* `geoip_update_configs`
## Key Design Decisions
| Decision | Reason |
| Decision | Rationale |
| --- | --- |
| Complete configuration versions, instead of online patches | Gives previews, activations, history, and rollbacks stable boundaries |
| Active pull by Agents | Server does not need SSH permissions, nor does it expose remote command execution entry points |
| Global single active version | Reduces MVP complexity and ensures all nodes are consistent by default |
| Website configurations aggregate multiple domains | Supports sharing site-level policies for a business site while allowing certificate binding per domain |
| Server-side aggregation of observability data | Avoids inconsistent results caused by temporary frontend calculations |
| Full Config Versioning instead of Patches | Provides stable, verifiable boundaries for previewing, activating, history, and rollbacks. |
| Pull Model (Agent-driven) | Server does not need SSH keys or inbound command ports, preventing control channel hijacking. Supports HTTP and WebSocket. |
| Global Single Active Version | Reduces MVP complexity, ensuring all nodes are uniform by default. Supports previews, version history, and one-click rollback. |
| Website Multi-Domain Aggregation | Enables sharing site-level policies across domains while supporting per-domain certificate binding. |
| Server-side Observability Aggregation | Prevents UI-side temporary statistical calculations from producing inconsistent data metrics. |
| Intranet Penetration based on frp | Reuses a mature tunnel protocol rather than custom implementations to minimize stability risks. frps Vhost routing aligns naturally with HTTP. |
| Independent Binary for Relay/Client | Separation of concerns: Relay manages frps, Client manages frpc, allowing independent updates and deployments. |
| Tunnel decoupled from Node system | Tunnel clients run internally, using completely different registration and authentication flows compared to edge nodes. |
## Contributor Reading Suggestions
## Recommended Reading for Contributors
If you want to modify architecture-related code, read these first:
Before modifying architectural code, please read:
1. [Product Boundary](./index.md)
2. [Release Model](./release-model.md)
3. [Development Constraints](./development.md)
4. [Repository Structure](../reference/repository.md)
1. [Product Boundaries](./index.md)
2. [Agent & Publish Model](./agent-design.md)
3. [Development Constraints](../../guildline/development-constraints.md)
4. [Repository Structure](./repository.md)
+125 -258
View File
@@ -1,315 +1,182 @@
# Development Constraints
# Local Development
You will learn: The admission criteria for OpenFlare code modifications, backend/Agent/frontend tiered constraints, data model boundaries, API conventions, database migration requirements, and test delivery baselines.
You will learn: How to build OpenFlare's local development environment, start the Server, the Agent, and the Admin Frontend, run test and build commands, and understand the boundaries to respect before contributing code.
This document integrates the original development specifications, frontend specifications, and development plans, and serves as the engineering constraints entry point for OpenFlare after `1.0.0`.
This page is aimed at contributors. Product boundaries, data model constraints, API conventions, and frontend layering specifications are governed by [Development Constraints](../../guildline/development-constraints.md); this page only provides actionable workflows for local development.
## Current Conclusions
## Repository Structure
* The mainline capabilities of the first to sixth versions have all been completed.
* `1.0.0` is the current official baseline.
* Procedural tasks of completed stages are subject to code, tests, and Git history.
* Priority for new work is given to bug fixes, maintainability improvements, and documentation and test reinforcement.
For details on the physical directory structure and responsibilities of each module (Server, Agent, Frontend, etc.), see [Repository Structure](./repository.md).
Current Development Priorities:
## Environment Requirements
1. Stability.
2. Upgrade and rollback link reliability.
3. Document accuracy.
4. Test coverage reinforcement.
5. Small iterations within existing boundaries.
## Change Admission
Before new requirements enter implementation, judge them in the following order:
1. Whether it fits the [Product Boundary](./index.md).
2. Whether it follows the backend, Agent, and frontend constraints in this document.
3. Whether it risks breaking the existing publish, sync, rollback, or upgrade main links.
4. Whether it requires synchronized updates to deployment, configuration, README, or documentation site pages.
If a requirement expands the boundary or introduces new infrastructure, the design documentation must be updated first before starting implementation.
Any changes merged into the official baseline must at least meet:
* Does not break the Agent heartbeat, synchronization, publishing, and rollback main links.
* Does not break the existing OpenResty main configuration hosting model.
* Does not degrade the existing availability of the overview, node details, and access analysis.
* Has tests or joint debugging verification commensurate with the risks.
* Documentation remains consistent with the code.
## Technical Baseline
Server:
* Go 1.25+
* Gin
* GORM
* SQLite / PostgreSQL
* Existing login system
Agent:
* Single binary
* Node-local execution
* Control OpenResty binary via `openresty_path` or default `openresty`
* Docker deployment uses the Agent image with built-in OpenResty, and does not have the Agent control a separate OpenResty container
Frontend:
* Next.js 15 App Router
* React 19
* TypeScript 5
* Tailwind CSS 4
* TanStack Query
* React Hook Form + Zod
* Zustand only used for lightweight client status
* ESLint + Prettier
* Vitest + Testing Library + Playwright
* pnpm
## Server Layering
| Directory | Responsibility |
| Item | Requirement |
| --- | --- |
| `controller/` | Parameter parsing, calling services, returning responses |
| `service/` | Business logic, verification, transaction orchestration, rendering |
| `model/` | Model definition and persistence |
| `router/` | Route registration |
| `middleware/` | Auth, authorization, rate limiting, and other cross-cutting logic |
| `common/` | Configuration, global state, and initialization entry points |
| `utils/` | Pure utility functions and general helpers |
| Go | `1.25+` |
| Node.js | `18+` |
| pnpm | Recommended enabling via `corepack enable` |
| Docker | Required for Server containers, local integration testing, and Agent Docker images |
| OpenResty | Required to execute `openresty` locally when running the Agent |
| PostgreSQL | Optional; if not configured, the Server defaults to SQLite |
It is forbidden to accumulate business logic in `controller/`, forbidden to implement business flows in `middleware/`, and forbidden to add platform-level abstractions for simple requirements.
## Initializing Frontend Dependencies
## Agent Layering
The Agent maintains its existing module boundaries:
* `config`
* `heartbeat`
* `sync`
* `openresty` / `nginx`
* `state`
* `httpclient`
* `protocol`
* `internal/updater`
Requirements:
* Each module has a single responsibility.
* External command calls are centrally encapsulated.
* State persistence and configuration persistence are separated.
## Frontend Layering
Recommended directories:
```text
app/
components/
features/
lib/
hooks/
store/
types/
styles/
tests/
```bash
cd openflare_server/web
corepack enable
pnpm install
```
Responsibility constraints:
Build the static assets hosted by the Go Server:
* `app/`: Routes, layouts, page assembly.
* `features/`: Organize modules by business domains.
* `components/`: Reuse components across features.
* `lib/`: Request client, environment variables, utility functions, constants.
* `store/`: A small amount of cross-page UI state.
* `types/`: Shared type definitions.
```bash
pnpm build
```
Page files are only responsible for obtaining routing parameters, organizing page structures, and calling feature components; they should not handwrite complex API details, complex form verification logic, or maintain a large amount of mutually coupled local states.
## Starting the Server
## Data Model Specifications
SQLite Mode:
Currently active entities:
```bash
cd openflare_server
export SESSION_SECRET='dev-session-secret'
export SQLITE_PATH='./openflare-dev.db'
export LOG_LEVEL='debug'
go run .
```
* `proxy_routes`
* `origins`
* `config_versions`
* `nodes`
* `auth_sources`
* `external_accounts`
* `node_system_profiles`
* `apply_logs`
* `tls_certificates`
* `managed_domains`
* `node_request_reports`
* `node_access_logs`
* `node_metric_snapshots`
* `traffic_analytics_rollups`
* `node_health_events`
* `options`
* `waf_rule_groups`
* `waf_rule_group_bindings`
PostgreSQL Mode:
General constraints:
```bash
cd openflare_server
export SESSION_SECRET='dev-session-secret'
export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable'
export LOG_LEVEL='debug'
go run .
```
* No new platform-oriented objects are added unless explicitly required by the design document.
* `origins` only serves as a reusable origin address directory, and the fields are kept lightweight.
* `proxy_routes` uses "site configuration" as the aggregation boundary and must contain a unique `site_name` and a non-empty `domains` list.
* Each domain in `proxy_routes.domains` must be globally unique, and the first item in the list is treated as the primary domain.
* `proxy_routes` continues to allow saving one or more upstream addresses for load balancing, but does not introduce an independent `origin_pool`.
* The legacy `domain` field can only be used as a compatible mirror of `domains[0]`; new code must not continue to use this field as the unique business input.
* If `proxy_routes` is associated with `origins`, it must also save the `origin_url` that can be directly rendered.
* Upstreams uniformly use named `upstream` + keepalive; for a single upstream carrying a base path or query, the original URI should be added back to `proxy_pass`. For multiple upstreams, only pure `scheme://host[:port]` is allowed.
* Rate limits, reverse proxy, and cache configurations currently belong to the site-level `proxy_routes`.
* HTTPS certificate binding must be saved on a per-domain basis through `domain_cert_ids` parallel to `domains`; domains not bound to a certificate must not participate in HTTPS rendering.
* WAF global rule groups are applied to all websites by default, while custom rule groups are bound to site configurations via `waf_rule_group_bindings`; they must be included in the complete configuration version snapshot during publishing.
* `config_versions` must save complete snapshots and rendering results.
* There can only be one activated version globally at a time.
* Rollback is achieved by reactivating older versions.
* `nodes` only retains control plane status and low-frequency summaries.
* Observability data must be associated with nodes and time windows, and snapshots and aggregation results use an append-only model.
* Original access details must have a controlled retention policy.
* `auth_sources` only saves management console third-party login source configurations, currently supporting `github` and `oidc`.
* `external_accounts` is the unique source of binding between third-party accounts and local users; the old `users.github_id` is only used for compatible migration and must not be used as the business input for the new login flow.
Default access URL:
## Database Migration
```text
http://localhost:3000
```
Any modification involving table structures, indexes, column types, sharding rules, or internal persistence metadata must upgrade the database version number in sync.
The default credentials are `root` / `123456`.
The database version number is defined in `openflare_server/model`, and it must not rely solely on `AutoMigrate` for implicit upgrades of existing databases.
## Starting the Frontend Dev Server
Every time the database version number is upgraded, an explicit migration method from the previous version to the new version must be added. The migration method must contain validation logic after the upgrade; only when the validation passes can the new database version record be written.
The frontend dev server listens to port `3001` by default and proxies requests to the backend via `NEXT_DEV_BACKEND_URL`:
Versions 1 through 7 are treated as the historical initial baseline and no longer keep per-version upgrade files. Starting from v8, database migrations must be placed under `openflare_server/model/migrate` and named after the target version, such as `v16.go`. Each version file registers its migration through `init()`, and the current database version is derived from the highest registered target version. Do not change the semantics of released v8+ migrations merely to reorganize files.
```bash
cd openflare_server/web
export NEXT_DEV_BACKEND_URL='http://127.0.0.1:3000'
pnpm dev
```
When performing a database upgrade, complete the following steps:
Access:
1. Decide whether a schema version bump is required: any addition, removal, or rename of tables, columns, indexes, constraints, column types, sharding rules, or persisted-data semantics must upgrade the version.
2. Add `openflare_server/model/migrate/vN.go`, where `N` is the target version. The file header must include a comment explaining what this upgrade changes and why it is needed.
3. Implement `VN()` in `vN.go`, and call `Register(VN())` from `init()`. `FromVersion` must be `N-1`, and `ToVersion` must be `N`.
4. Implement the upgrade logic in `migrateVN`. Use `Context` to call shared capabilities such as `ApplyCurrentSchema`, historical backfills, and default-data initialization; complex data repairs must be explicit and must not rely on `AutoMigrate` alone.
5. Implement post-upgrade validation in `validateVN`. Validation must cover at least the existence of new tables/columns/indexes, required default data, and required data backfills.
6. If the migration needs new shared backfill or validation helpers, place them in `openflare_server/model/migrations.go` or another suitable model file, and expose them through `Context` to `model/migrate`; avoid reverse-importing `model` from the subpackage and creating an import cycle.
7. Add migration tests covering at least upgrade from the `N-1` old database to `N`, including schema version, table/column structure, key data backfills, and validation results. The `model/migrate` registry test checks version continuity, but business-specific migrations still require tests.
8. Update design/development docs; if management APIs, configuration fields, or user-visible behavior change, also update the relevant guides, configuration reference, and Swagger documents.
```text
http://localhost:3001
```
After starting the new package, the database's current version must be checked first, and then upgraded step by step in order to the target version; skipping intermediate upgrade steps to directly write the target version is prohibited.
## Starting the Agent
An empty database initialization can directly establish the current version structure, but the same-version validation must still be executed after the initialization is completed, and the current database version must be persisted.
If the migration or validation fails, the startup process must abort, and the database version record must not be upgraded. Submissions involving database version changes must add corresponding migration tests or equivalent regression tests.
## API and Authentication
The management console and Agent APIs uniformly use JSON. Both success and failure must return a clear `message`:
Create a local `agent.json`:
```json
{
"success": true,
"message": "",
"data": {}
"server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token",
"data_dir": "./data",
"heartbeat_interval": 10000,
"request_timeout": 10000
}
```
Conventions:
Run:
* Agent APIs are uniformly placed under `/api/agent/*`.
* The overview and node details prioritize using dedicated aggregation interfaces.
* Management console mutation APIs uniformly use `POST`; read-only APIs use `GET`.
* The management console continues to reuse existing logins, roles, and Sessions.
* Third-party login uniformly enters through authentication source APIs; authentication source management interfaces must require Root Session.
* `/api/status` can only return the public fields of enabled authentication sources, and must not return the Client Secret.
* When a third-party account is not bound and registration is closed, a process to bind to an existing account should be provided, and users must not be automatically created.
* Official Agent requests uniformly use the node-exclusive `agent_token`.
* The first access can use the global `discovery_token`.
* Agent request headers uniformly use `X-Agent-Token`.
```bash
cd openflare_agent
export LOG_LEVEL='debug'
go run ./cmd/agent -config ./agent.json
```
It is forbidden to expose remote shell or arbitrary command execution entries, forbidden to print full Tokens in logs, and forbidden to save main configuration templates that bypass placeholder constraints.
If `openresty_path` is not configured, the Agent calls `openresty` by default. For debugging, you can explicitly configure `openresty_path`, `main_config_path`, `route_config_path`, `access_log_path`, `cert_dir`, `lua_dir`, and `runtime_config_dir`.
## Publishing and Runtime
## Running Tests
The publishing logic must maintain:
Server:
* Read all enabled `proxy_routes` during publishing.
* Read OpenResty main configuration parameters, reverse proxy performance parameters, and cache parameters at the same time.
* Generate complete OpenResty configuration.
* Calculate `checksum`.
* Write to `config_versions`.
* Activate the version by switching `is_active`.
```bash
cd openflare_server
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Version constraints:
Agent:
* The version number format is fixed as `YYYYMMDD-NNN`.
* Do not modify historical versions online.
* Do not make differentiated versions grouped by nodes.
* Preview and diff are read-only capabilities and do not generate release records.
```bash
cd openflare_agent
GOCACHE=/tmp/openflare-go-cache go test ./...
```
The Agent must satisfy:
Frontend:
* Read or generate local `node_id` after startup.
* Periodic heartbeats and synchronization.
* Conventional synchronization prioritizes judging based on the version summary returned by the heartbeat.
* When WS connection upgrade is enabled and the connection is successful, the Agent can receive active version summaries via WS and immediately synchronize; WS failure or disconnection must fall back to HTTP heartbeats.
* Back up old files first when discovering a new version.
* Write main configurations, route configurations, and necessary certificate files.
* Write WAF/PoW runtime configurations, and ensure WAF Lua resources are managed uniformly by the Agent.
* Execute `openresty -t -c <main_config_path>` after writing the new configuration, and then reload; direct startup of OpenResty is allowed when reload finds that it is not running.
* Periodic runtime health checks must not call `openresty -t`, preventing health probes from triggering synchronous upstream domain name resolutions; they should prioritize requesting `/openflare/stub_status` on the local `openresty_observability_port`, using HTTP `200 OK` as the basis for judging that the OpenResty main process and workers are serving.
* If the activation of the new configuration fails, the Agent must first try to restore execution with the target configuration, then roll back to the old configuration and pull up OpenResty again.
* Report warning when OpenResty recovers normally after rollback; if there is no historical main configuration to restore locally, it must be allowed to write the built-in safe fallback configuration and pull up an OpenResty runtime state that only listens to port `80` externally and uniformly returns `503 Service Unavailable` and `OpenFlare: No Valid Configuration`, while retaining the local `stub_status` health check entry. The fallback runtime state must not clear the blocked status of the failed target; the application logs must reflect that the target version failed but the fallback runtime has started. Report failure when there is a historical main configuration but it still cannot recover after rollback.
* Once a target `version + checksum` application fails and rolls back, the Agent must block repeated applications of this target in its local state.
* When the Agent maintains the local MaxMind mmdb, download or refresh failures can only record warnings, and must not block heartbeats, synchronization, configuration application, or OpenResty health checks.
```bash
cd openflare_server/web
pnpm lint
pnpm typecheck
pnpm test
pnpm test:e2e
```
## Frontend Requests, State, and Types
Docs:
All API requests must be uniformly routed through `lib/api/`:
```bash
cd docs
pnpm build
```
* Uniformly handle the `success/message/data` response structure.
* Uniformly handle authentication failure, network exceptions, and general error messages.
* Centralize maintenance of resource interfaces and request paths.
## Building
State Layering:
Admin static assets:
* Server state: TanStack Query.
* Page temporary state: Component-internal `useState`.
* Cross-page UI state: Zustand.
```bash
cd openflare_server/web
pnpm build
```
Strict TypeScript mode is required; abuse of `any` is prohibited. API responses, form inputs, and business entities must have explicit types.
Server binary:
## Forms, Interaction, Style, and Themes
```bash
cd openflare_server
go build -o openflare-server .
```
Forms uniformly use React Hook Form and Zod.
Agent binary:
High-risk operations must have double confirmation, show the name of the operation object, and clearly provide success and failure feedback.
```bash
cd openflare_agent
go build -o openflare-agent ./cmd/agent
```
Style principles:
## Debugging Entrypoints
* Uniformly use Tailwind CSS and the existing token system.
* Prioritize reusing existing basic components and layout components.
* Maintain consistent visual hierarchy, padding, and semantic colors.
| Context | Command or Path |
| --- | --- |
| Server Logs | `LOG_LEVEL=debug go run .` |
| Agent Logs | `LOG_LEVEL=debug go run ./cmd/agent -config ./agent.json` |
| Swagger Docs | `http://localhost:3000/swagger/index.html` |
| Frontend API Proxy | `NEXT_DEV_BACKEND_URL=http://127.0.0.1:3000 pnpm dev` |
| OpenResty Validation | `openresty -t -c ./data/etc/nginx/nginx.conf` |
Theme requirements:
## Code Style & Change Admission
* Support `light`, `dark`, and `system` simultaneously.
* User choices must be persisted.
* Try to avoid theme flickering on the first screen.
Before contributing, verify:
## Test and Delivery
1. The requirement matches [Product Boundaries](./index.md).
2. The implementation conforms to [Development Constraints](../guildline/development-constraints.md).
3. The change does not disrupt publishing, sync, rollback, or upgrading lifecycles.
4. Update corresponding documentation if configurations, deployments, APIs, or boundaries change.
5. High-risk edits must be accompanied by unit tests or equivalent integration testing.
* Key business logic must have unit tests or equivalent regression tests.
* Agent main link modifications must verify synchronization, application, and rollback.
* Frontend pages must cover at least loading states, empty states, error states, and success feedback.
* When the Go version is adjusted, check `go.mod`, Dockerfile, and CI workflows in sync.
## Subsequent Maintenance
Subsequent planning is no longer maintained in the form of "major version phase documents", but adopts the following methods:
* Product boundary changes: Update [Product Boundary](./index.md).
* Engineering constraint changes: Update this document.
* Deployment and configuration changes: Update [Deployment Guide](../guide/deployment.md), [Configuration Items](../reference/configuration.md), and README.
If explicit new stage goals appear in the future, add dedicated planning documents separately; do not pile completed historical plans back into this document.
The model boundary of the current special topic "Site-level Rules and Configuration Interface Reconstruction" has been integrated into the [Product Boundary](./index.md). When executing, still advance in the order of data models, interfaces, frontend pages, migration tests, and document linkage.
Database schema alterations must elevate the database version number and supply explicit migration and validation methods from the previous version.
+151 -118
View File
@@ -1,171 +1,204 @@
# Product Boundary
# Product Boundaries
You will learn: What OpenFlare is, what problems it solves, who the target users are, what the current stable capabilities are, and which design boundaries cannot be bypassed during implementation.
You will learn: What OpenFlare is, what problems it solves, who the target audience is, what current stable features are available, and which design boundaries cannot be bypassed during implementation.
OpenFlare is a self-hosted OpenResty control plane oriented toward single-team or single-organization internal operation and maintenance (O&M) scenarios. It resolves the issues of scattered management in reverse proxy configuration, node synchronization, certificate hosting, configuration release/rollback, and basic observability.
OpenFlare is a self-hosted OpenResty control plane designed for single-team or single-organization internal operations. It solves the problems of decentralized management of reverse proxy configurations, node synchronization, certificate hosting, configuration publication and rollback, and basic observability.
## Project Positioning
OpenFlare is suitable for teams that need to centrally manage multiple OpenResty proxy nodes:
* Want to maintain reverse proxy site configurations using a management console.
* Want every configuration change to have a complete version, preview, activation, and rollback.
* Want nodes to actively sync configuration, rather than having the control plane SSH into nodes to execute commands.
* Want to manage TLS certificates, domain assets, node statuses, and basic access analytics within the same system.
* Wanting to maintain reverse proxy website configurations using a management dashboard.
* Wanting every configuration change to have a complete version history, preview, activation, and rollback support.
* Wanting nodes to actively synchronize configurations, rather than the control plane SSHing into nodes to execute commands.
* Wanting to manage TLS certificates, domain assets, node statuses, and basic access analytics in a single system.
OpenFlare is currently not positioned as a general-purpose log platform, service mesh, Kubernetes Ingress Controller, or multi-tenant cloud platform.
OpenFlare is currently not positioned as a general-purpose logging platform, service mesh, Kubernetes Ingress Controller, or multi-tenant cloud platform.
## Target Users
| User | Needs |
| --- | --- |
| Self-hosted users | Quickly deploy a visual OpenResty control plane |
| Internal O&M teams | Manage multiple reverse proxy nodes, certificates, and configuration versions |
| Development teams | Provide a unified entry point and basic access analytics for internal services |
| Contributors | Fix defects, strengthen tests, and improve documentation within clear boundaries |
## Current Stable Capabilities
## Current Capabilities
| Capability | Description |
| --- | --- |
| Reverse Proxy Rule Management | Uses site configuration as the aggregation boundary, supporting multi-domain and origin configuration |
| Site-level Configuration | One rule corresponds to one site, which can bind one or more domains and share site-level configuration |
| Origin Management | Maintains a lightweight origin directory and allows sites to save renderable origin snapshots |
| Configuration Versioning | Supports preview, publishing, activation, immutable history, and rollback |
| Agent Synchronization | Supports registration, heartbeat, synchronization, application result reporting, and self-updating |
| OpenResty Hosting | Manages main configuration templates, performance parameters, cache parameters, and Lua resources |
| HTTPS/TLS | Hosts certificates and domain assets, and binds certificates on a per-domain basis |
| WAF | Maintains IP/IP ranges black/whitelists and country-level geographical black/whitelists with global and website-customized rule groups |
| Basic Observability | Aggregates node requests, resource snapshots, health events, and access analytics |
| Node Management | Node status, token systems, deployment, and update links |
| Console Frontend | Next.js-based official management console |
| Auth Source Login | Supports configuring GitHub and standard OIDC login entries as authentication sources, allowing third-party accounts to bind to existing local users |
| Reverse Proxy Rules | Uses website configuration as the aggregation boundary, supporting multiple domains and origin settings. |
| Website-level Config | One rule corresponds to one website, which can bind one or more domains and share site-level configurations. |
| Origin Management | Maintains a lightweight origin directory and allows websites to save renderable origin snapshots. |
| Config Versioning | Supports previews, publishing, activation, immutable history, and rollbacks. |
| Agent Sync | Supports registration, heartbeats, synchronization, application result reporting, and self-updating. |
| OpenResty Hosting | Manages main config templates, performance parameters, cache parameters, and Lua resources. |
| HTTPS/TLS | Hosts certificate and domain assets, binding certificates on a per-domain basis. |
| WAF | Maintains IP/CIDR block blacklists/whitelists, IP groups, and country-level geographic access controls at both global and site-specific levels. |
| Basic Observability | Aggregates node requests, resource snapshots, health events, and access analytics. |
| Node Management | Manages node status, token systems, and deployment/update lifecycles. |
| Admin UI | Next.js-based official management dashboard. |
| Auth Source Login | Supports configuring GitHub OAuth and standard OIDC login portals, allowing third-party accounts to bind to existing local users. |
| Intranet Penetration | Securely exposes intranet HTTP services to the public internet using TunnelRelay nodes and the OpenFlared client, reusing the Agent's HTTPS/WAF capabilities. |
Default working method:
Default Working Model:
* All nodes consume the same globally activated version.
* The Server saves configuration and status, and does not directly manage nodes via SSH.
* The Agent is the only controlled landing entry point on the node side.
* All nodes consume the same globally activated configuration version.
* The Server stores configurations and state, and does not directly SSH to manage nodes.
* The Agent is the only controlled entry point on the node side.
* TunnelRelay nodes run both the Agent (OpenResty) and the Relay (frps manager) to provide intranet penetration relays.
* The OpenFlared client runs inside the intranet, managing the frpc process to connect to the Relay and forward traffic to intranet services.
## Typical Use Cases
| Scenario | Description |
| --- | --- |
| Unified Entry for Internal Services | Expose multiple internal HTTP services through a unified domain and certificate |
| Config Sync for Multi-node Reverse Proxy | Multiple OpenResty nodes consume the same activated configuration |
| Config Change Review | View preview or diff before publishing, and retain immutable history after publishing |
| Quick Rollback | Reactivate an older version, letting the Agent pull and apply it |
| Certificate Hosting | Bind TLS certificates for different domains |
| Basic Observability | View node status, request aggregation, access analytics, and health events |
| Unified Entrance | Exposes multiple internal HTTP services via a unified domain and TLS certificate. |
| Multi-Node Sync | Multiple OpenResty nodes consume the same active configuration version. |
| Change Review | View previews or diffs before publishing, keeping an immutable history post-publish. |
| Rapid Rollback | Re-activate an older version, letting the Agent pull and apply it. |
| Certificate Hosting | Bind TLS certificates to different domains under the same website. |
| Observability | Check node health status, aggregated requests, traffic analytics, and health events. |
| Intranet Penetration | Exposes intranet HTTP services that are not directly reachable from the public internet using Tunnels, benefiting from HTTPS, WAF, and all other protections. |
## Core Objects
## Website Configuration Constraints
Currently active entities:
* `proxy_routes`
* `origins`
* `config_versions`
* `nodes`
* `auth_sources`
* `external_accounts`
* `node_system_profiles`
* `apply_logs`
* `tls_certificates`
* `managed_domains`
* `node_request_reports`
* `node_access_logs`
* `node_metric_snapshots`
* `traffic_analytics_rollups`
* `node_health_events`
* `waf_rule_groups`
* `waf_rule_group_bindings`
## Site Configuration Constraints
`proxy_routes` is upgraded from a "single-domain rule" to a "site configuration" aggregate object. One record corresponds to one website, which can bind one or more domains and share a set of site-level configurations.
`proxy_routes` is the aggregate object for "website configurations". One record corresponds to one website, which can bind one or more domains and share a set of site-level configurations.
Constraints:
* `proxy_routes.site_name` is the unique business identifier of the website.
* `proxy_routes.domains` contains at least one domain, and `domains[0]` is used as the primary domain.
* `proxy_routes.domains` must contain at least one domain, and `domains[0]` is treated as the primary domain.
* Any domain can globally belong to only one `proxy_routes`.
* During the migration period, `proxy_routes.domain` can be kept as a mirror field of `domains[0]`, but business read/write and subsequent extensions must be based on `site_name` + `domains`.
* Site-level rate limits, reverse proxies, and cache configurations are currently shared by site and are not configured differently on a per-domain basis within the same website.
* HTTPS allows binding certificates per domain within the same site.
* Site-level rate limits, reverse proxies, and caching configurations are shared by the site, with no per-domain differences allowed within the same website.
* HTTPS allows binding certificates on a per-domain basis within the same site.
## Origin Constraints
## Origin & Upstream Constraints
`origins` only saves the origin address, display name, and remarks, and does not carry protocols, ports, paths, weights, or health check policies.
`origins` serve the reuse of the origin directory, storing only the origin address, display name, and remarks, without carrying protocols, ports, paths, weights, or health check policies. `proxy_routes` can optionally associate with an `origins` record, but the rule internally still saves a complete upstream snapshot for rendering.
`proxy_routes` can optionally associate an `origins` record to reuse the origin address; the rule still saves a complete `origin_url` snapshot to participate in rendering and version snapshots.
Upstream Constraints:
Upstream constraints:
* `proxy_routes` must contain at least one upstream address (for direct type `direct`), or be associated with a Tunnel (for intranet penetration type `tunnel`).
* Multi-upstream load balancing is uniformly rendered into a named `upstream` with keepalive enabled.
* A single upstream is allowed to carry a base path or query, which is appended in `proxy_pass`. Multi-upstream is strictly limited to pure `scheme://host[:port]` structures, and all upstreams in the same rule must use the same protocol.
* `proxy_routes.origin_host` is an optional field used to override the `Host` header during back-to-source requests.
* All direct upstream addresses must be valid `http://` or `https://` URLs.
* Intranet penetration upstreams must associate with a valid `tunnel_id` and specify the intranet target address and protocol.
* `proxy_routes` must contain at least one upstream address.
* To maintain compatibility with historical data, the `origin_url` main upstream field is retained, and multiple upstreams are allowed to be added within the same rule for load balancing.
* Upstreams are rendered uniformly as a named `upstream` with keepalive.
* A single upstream can carry a base path or query and append it in `proxy_pass`.
* Multiple upstreams are restricted to pure `scheme://host[:port]`.
* `proxy_routes.origin_host` is an optional field, used to override the `Host` request header when back-origin.
* All upstream addresses must be legal `http://` or `https://`.
## Intranet Penetration Constraints
OpenFlare implements intranet penetration through TunnelRelay nodes and the OpenFlared client, built on top of frp (Fast Reverse Proxy).
### Node & Component Model
**Node Types**:
* `nodes.node_type` distinguishes the node type: `edge_node` (edge node, default) and `tunnel_relay` (tunnel relay).
* TunnelRelay nodes run both the Agent (OpenResty) and the Relay (frps manager) concurrently, sharing the same `agent_token`.
- The Agent is responsible for HTTPS termination, WAF protection, caching, and rate limiting.
- The Relay manages the frps process, providing tunnel relay services for intranet clients.
* TunnelRelay nodes introduce new fields: `node_type`, `relay_bind_port` (frpc connection port, default 7000), `relay_vhost_http_port` (HTTP Vhost port, default 8080), `relay_auth_token` (automatically generated), `relay_status`, etc.
**Tunnel Client**:
* The `tunnels` table independently stores intranet penetration client registration info and is decoupled from the `nodes` system.
* Each Tunnel has a unique `tunnel_id` (format `tun-<32hex>`) and `tunnel_token` (client authentication credential).
* The OpenFlared client runs inside the intranet, is not exposed to the public internet, uses `tunnel_token` for authentication, and communicates with the Server via `/api/flared/*` endpoints.
* An OpenFlared client can connect to multiple Relays simultaneously for high availability.
### Upstream Type Expansion
The upstream configuration of `proxy_routes` is divided into two types, distinguished by the `upstream_type` field:
* **Direct Upstream (`direct`, default)**: Forwards traffic directly to the origin address, behaving exactly like the existing mechanism.
* **Intranet Penetration Upstream (`tunnel`)**: Forwards traffic to the intranet service via a TunnelRelay node.
- Must specify `tunnel_id` (associated with the `tunnels` table).
- Must specify `tunnel_target_addr` (intranet target address, e.g., `192.168.1.100:8080`) and `tunnel_target_protocol` (`http` or `https`).
- During publication, the Server automatically replaces the upstream address with `http://127.0.0.1:{relay_vhost_http_port}`.
### Traffic Paths & Protocols
**Complete Data Plane Traffic Path**:
```
Browser → OpenResty (Agent, TLS/WAF) [TunnelRelay Node]
↓
frps (Relay, HTTP Vhost Routing) [TunnelRelay Node, 127.0.0.1:{vhost_port}]
↓
frp Tunnel Protocol (Host Header Routing)
↓
frpc (Client, Multi-process) [Intranet Server]
↓
Intranet Service (192.168.x.x:port)
```
**Key Features**:
* frps uses the HTTP Vhost single-port reuse mechanism; all HTTP tunnels share one `vhost_port`, automatically routed to the corresponding frpc based on the Host header.
* The Agent preserves the original `Host` header, which frps uses to match the virtual host.
* Each tunnel corresponds to a single `proxy_routes` and can bind multiple domains.
* The OpenFlared client manages an independent frpc process for each connected Relay, transmitting multiple HTTP proxy definitions via a single frp tunnel.
### Configuration Sync Model
The publication process generates two types of configuration version data simultaneously, linked by a single `config_version` version number:
* **Agent-side Config**: OpenResty main configuration + route configurations + WAF rules. If a tunnel upstream is included, it is automatically rendered as a `http://127.0.0.1:{vhost_port}` upstream.
* **Tunnel-side Config**: Relay list + frpc proxy definitions. Versioned alongside the publishing process; changes are hot-reloaded using `frpc reload` first.
* **Relay Config**: Dispatched via heartbeat responses, relatively static, and not included in the versioned publishing flow.
### Tunnel Design Constraints
* Only HTTP protocol tunnel traffic is supported (keeping TCP/UDP tunnels extensible); separate TCP/UDP port allocation is not supported for now.
* The DNS for domains using Tunnel upstreams should resolve to the designated TunnelRelay node.
* frp binaries (v0.61+) are packaged and provided by the system deployment script or container images.
## HTTPS Constraints
`proxy_routes.domain_cert_ids` is used to record domain-certificate bindings parallel to `domains`; a value of `0` indicates that HTTPS is not enabled for the domain, retaining only HTTP.
`proxy_routes.domain_cert_ids` is used to record the domain-certificate bindings parallel to `domains`; a value of `0` means the domain does not have HTTPS enabled and stays HTTP-only.
During publishing rendering:
During rendering:
* Domains with certificates are output as separate `443 ssl` `server` blocks grouped by certificate.
* Domains not bound to a certificate must not be automatically brought into HTTPS.
* All domains in `proxy_routes.domains` must be included in the same site configuration to avoid the same site being split in version snapshots.
* Domains with certificates are grouped by certificate and output as independent `443 ssl` `server` blocks.
* Domains without certificates bound must not be automatically routed to HTTPS.
* All domains in `proxy_routes.domains` must be kept in the same site configuration to avoid being split across version snapshots.
## WAF Constraints
WAF uses rule groups as configuration boundaries. The system fixes a global rule group, which is applied to all websites by default; websites can overlay multiple custom rule groups.
WAF centers around rule groups. The system provides a single global rule group (applied to all sites by default), on top of which websites can overlay multiple custom rule groups.
Phase 1 supports:
Core Capabilities:
* IP / IP range whitelists and blacklists.
* Country-level region whitelists and blacklists.
* Rule group-level blocking status codes and response pages, defaulting to `418` and an empty page.
* Supports individual IP / CIDR block whitelists and blacklists.
* Supports IP group references (including manual, automatic Expr calculated, and URL subscribed IP groups).
* Supports GeoIP-based country/region level admission filtering.
* Supports custom interception responses for rule groups (custom status codes and interception HTML pages, default is `418`).
Evaluation order:
IP Group & Judgment Constraints:
* Whitelists are bypass exceptions; if any enabled rule group matches a whitelist, the request is allowed.
* If no whitelist is matched, blacklists continue to be evaluated.
* When multiple blacklists match, the global rule group takes precedence, followed by custom rule groups in ascending order of their IDs.
Region recognition is based on the MaxMind mmdb maintained locally on the node by the Agent, and the OpenResty Lua reads the local database during the request path. When GeoIP dependencies are unavailable, region rules must be skipped, without affecting IP rules and the reverse proxy main link.
* **Runtime Decoupling**: The WAF runtime only reads local JSON files and does not access the Server database; configuration versions only store referenced IP group IDs. IP group members are synchronized via MD5 checksum differences and WebSocket push notifications, achieving hot activation without reloading Nginx.
* **Built-in Expr Rules**:
* High-frequency 404 scanning block: `request_count > 100 && status_404_ratio >= 0.8`
* Malicious IP direct probe: `ip_host_count > 50 && ip_host_ratio > 0.5`
* **Decision Priority**: The whitelist has absolute priority. If it does not match the whitelist, the blacklist funnel is triggered (global rule group first, custom groups matched in ascending ID order).
* GeoIP resolution depends on the local MaxMind database; if GeoIP is anomalous, region rules are automatically ignored and must not disrupt the availability of IP rules and the main reverse proxy chain.
## Authentication Source Constraints
`auth_sources` is the configuration object for third-party login entries on the management console, currently supporting only two types: `github` and `oidc`. Enabled authentication sources will be displayed on the login page.
`auth_sources` uniformly supports `github` and `oidc` login configurations. `external_accounts` stores bindings between third-party accounts and local users. Logic for first-time third-party login:
`external_accounts` saves the binding relationship between external accounts of authentication sources and local users. When a third-party account logs in for the first time:
* If already bound, directly authorize login; if there is an active local session, automatically bind.
* If unbound and registration is enabled, automatically create a local account; if registration is closed, require the user to provide an existing local username and password to establish the association.
* If it is bound to a local user, it logs in directly.
* If there is an existing local login session, it binds to the current user.
* If it is not bound and registration is allowed, a normal user is automatically created and bound.
* If it is not bound and registration is closed, the user is only allowed to enter an existing local account and password to complete the binding.
## Version & Observability Constraints
The old `users.github_id` only serves as a source for upgrade migration; new third-party account login and binding relationships must be based on `external_accounts`.
## Version and Observability Constraints
* `config_versions` must save complete snapshots, rendering results, and `checksum`.
* There can only be one activated version globally at a time.
* Rollback is achieved by reactivating older versions.
* `nodes` only carries control plane status and low-frequency summaries, not high-frequency observability facts.
* Metrics, trends, and access analytics prioritize server-side aggregation results, rather than temporary frontend statistics.
* Access details are only retained for controlled time windows, not evolving into a general-purpose log platform.
* `config_versions` must save the complete snapshot, rendering result, and `checksum`.
* Globally, only one version can be active at a time.
* Rollback is achieved by re-activating an older version.
* `nodes` only carry control plane state and low-frequency summaries; they do not carry high-frequency observability facts.
* Metrics, trends, and access analytics prioritize server-side aggregation rather than client-side temporary statistics.
* Access detail logs are only retained within a controlled time window, not evolving into a general logging platform.
## Documentation Maintenance Principles
* Update this document when the product scope or system boundary changes.
* Update this document when the product range or system boundaries change.
* Update [System Architecture](./architecture.md) when the system structure or module responsibilities change.
* Update [Release Model](./release-model.md) when the release, synchronization, or rollback model changes.
* Update [Development Constraints](./development.md) when development constraints, code specifications, or interface conventions change.
* Update [Deployment Guide](../guide/deployment.md) and README when deployment methods change.
* Update [Configuration Reference](../reference/configuration.md) when configuration items change.
* Completed phases will no longer be backfilled in the form of "version plans".
* Before starting a new phase, complete the design first, then enter implementation.
* Update [Agent & Publish Model](./agent-design.md) when the publishing, synchronization, rollback, or Agent model changes.
* Update [Development Constraints](../../guildline/development-constraints.md) when developer constraints, code specifications, or API conventions change.
* Update README and [Deployment Instructions](../../deployment/deployment.md) when deployment methods change.
* Update [Configurations Reference](../reference/configuration.md) when configuration items change.
* Completed phases should no longer be backfilled as "version plans".
* Before starting a new phase, complement the design first, then proceed to implementation.
-73
View File
@@ -1,73 +0,0 @@
# Release Model
You will learn: Why OpenFlare uses a complete configuration version as the release unit, and how publishing, activation, Agent application, and rollback work.
OpenFlare's release model is centered on complete configuration versions rather than modifying node configurations online.
Standard link:
```text
Modify rules -> Preview / View diff -> Publish -> Generate complete configuration version -> Activate version -> Agent pulls -> Local application -> Report result
```
## Publishing Rules
When publishing, the Server must:
1. Read all enabled `proxy_routes`.
2. Read the Server side OpenResty main configuration template, performance parameters, cache parameters, and necessary Lua resources.
3. Read domain and certificate binding relationships.
4. Read the WAF global rule group, custom rule groups, and website binding relationships.
5. Render the complete OpenResty configuration and WAF runtime configuration.
6. Calculate the `checksum`.
7. Write to `config_versions`.
8. Switch the activated version.
9. Let the Agent discover and apply it in subsequent heartbeats.
The version number format is fixed as `YYYYMMDD-NNN`.
## Preview and Publishing
Preview and diff are read-only capabilities and do not generate release records.
Publishing generates a new complete configuration version. The version must contain sufficient information so that future rollbacks can be re-applied based on historical snapshots, without relying on current mutable configurations.
## Activating Version
There can only be one activated version globally at a time. Differentiated versions grouped by nodes are currently not supported.
The Agent obtains the activated version summary through the heartbeat; only when the remote version or checksum is inconsistent with the local state does the Agent enter the synchronization flow. When Agent WS connection upgrade is enabled and the connection is available, the Server will broadcast the latest active version summary after successfully publishing or activating a version. Upon receiving it, the Agent immediately pulls and applies the configuration using the ordinary synchronization flow. When WS is unavailable, changes are still discovered at HTTP heartbeat intervals.
## Immutable History
Historical versions are immutable. Rollback is not achieved by modifying older versions, but by reactivating older versions.
The result of doing this is:
* Every version can be traced back.
* The rollback link is consistent with the ordinary release application link.
* The Agent does not need to understand "reverse patch", but only needs to apply a target version.
## Agent Application Policy
When discovering a new version, the Agent will:
1. Pull the details of the target version.
2. Back up old files.
3. Write the main configuration, route configurations, certificates, necessary Lua resources, and WAF/PoW runtime configurations.
4. Execute OpenResty configuration verification.
5. reload; if it is not started during runtime, try to start OpenResty with the current configuration.
6. Report success, warning, or failure.
If the activation of the new configuration fails, the Agent must try to restore execution; report a warning when the rollback succeeds. If there is no historical main configuration to roll back to locally, the Agent will write the built-in safe fallback configuration and try to pull up OpenResty: this configuration only listens to port `80` externally, contains no user routes, uniformly returns `503 Service Unavailable` and `OpenFlare: No Valid Configuration`, and retains the local `stub_status` health check entry. If fallback startup is successful, it still blocks the failed target version and reports a warning; report a failure when there is a historical main configuration but it still cannot recover after rollback.
Once a target `version + checksum` application fails and rolls back, the Agent will block repeated applications of this target in its local state. Only when the remote activated version or checksum changes is it allowed to try again.
## Design Constraints
* Publishing must read all enabled site configurations, rather than only rendering the modified object this time.
* Rollback is achieved by reactivating older versions, without modifying historical versions.
* The Agent API is fixed to use the node-exclusive `agent_token`; the first access can use the `discovery_token`.
* The Server does not provide remote shell or arbitrary command execution entries.
* The configuration version must save complete snapshots, rendering results, and `checksum`.
* WAF rule groups and website binding relationships must enter the snapshot and checksum along with the complete configuration version, and must not rely on the current mutable WAF configuration when rolling back.
+93
View File
@@ -0,0 +1,93 @@
# Repository Structure
You will learn: The responsibilities of Server, Agent, Frontend, scripts, and documentation folders in the OpenFlare repository, and where to place logic when contributing code.
| Path | Responsibility |
| --- | --- |
| `openflare_server` | Gin + GORM + SQLite/PostgreSQL single monolithic control plane |
| `openflare_server/web` | Next.js 15 App Router Admin Frontend, hosted by Go Server |
| `openflare_agent` | Go monolithic Agent running on the node side |
| `openflare_relay` | Tunnel relay daemon running on public edges, managing frps processes |
| `openflared` | Tunnel client running on intranet servers, managing frpc processes |
| `scripts` | System helper scripts for installation, self-updating, etc. |
| `docs` | VitePress documentation website, design baselines, specifications, and configurations |
| `docs/en` | English version of documentation |
## Server Layering
| Folder | Responsibility |
| --- | --- |
| `controller/` | Parameter parsing, service calling, and returning responses |
| `service/` | Business logic, validations, transaction orchestration, and configuration rendering |
| `model/` | Model definitions, database versioning, and migrations |
| `router/` | Route registration |
| `middleware/` | Cross-cutting concerns like authentication, authorization, rate limiting, CORS, and Turnstile |
| `common/` | Configurations, global states, and initialization entrypoints |
| `utils/` | Pure utility functions and general helpers |
| `job/` | Periodic cron tasks (such as SSL certificate auto-renewals) |
| `upload/` | File upload handlers |
| `docs/` | API documentation (Swagger) |
| `data/` | Static data (such as GeoIP databases) |
## Agent Modules
| Module | Responsibility |
| --- | --- |
| `config/` | Configuration loading and default values |
| `heartbeat/` | Heartbeat check-in and configuration version evaluation |
| `sync/` | Configuration fetching and application orchestration |
| `nginx/` | OpenResty file writing, validation, reloads, startup, and rollbacks |
| `state/` | Local states and buffers for metric reporting |
| `httpclient/` | Server HTTP API communication |
| `wsclient/` | WebSocket client communication |
| `protocol/` | Agent API protocol types and structures |
| `updater/` | Agent self-updating logic |
| `logging/` | Logging processing |
| `observability/` | Observability (metrics, tracing, etc.) |
| `geoipdata/` | GeoIP database handling |
| `geoipupdate/` | GeoIP database updates |
| `agent/` | Core Agent bootstrap and lifecycle orchestration |
## Frontend Layering
| Folder | Responsibility |
| --- | --- |
| `app/` | Next.js App Router routes, layouts, and page assemblies |
| `features/` | Feature modules organized by business domains |
| `components/` | Reusable UI components shared across features |
| `lib/` | API clients, environment configurations, utility functions, and constants |
| `store/` | Lightweight cross-page UI state management |
| `types/` | Shared TypeScript type definitions |
| `styles/` | Global stylesheets |
| `tests/` | Frontend unit and integration tests (Vitest, Playwright) |
| `scripts/` | Build and deployment scripts |
| `public/` | Static assets |
## Relay Modules
| Module | Responsibility |
| --- | --- |
| `cmd/` | CLI startup entrypoint and main bootstrap functions |
| `internal/config/` | Local configurations parsing and defaults initialization |
| `internal/frps/` | Manages the lifecycle of the frps process, monitoring its status |
| `internal/heartbeat/` | Periodic HTTP heartbeat, status reporting, and update retrievals |
| `internal/httpclient/` | General API client for calling the Server |
| `internal/observability/` | Host and frps metrics collection and pre-aggregation |
| `internal/relay/` | Coordinates the core Relay lifecycle, setup, and cleanup |
| `internal/state/` | Local runtime states, error logs, and persistent caches |
| `internal/updater/` | Relay update check, download installation, and restarts |
| `internal/wsclient/` | Bi-directional real-time WebSocket connection to the Server |
## OpenFlared (Client) Modules
| Module | Responsibility |
| --- | --- |
| `cmd/` | CLI startup entrypoint and main bootstrap functions |
| `internal/config/` | Local client configurations loading and parsing |
| `internal/flared/` | Core client scheduling and tunnel lifecycle orchestration |
| `internal/frpc/` | Dynamically generates `frpc.toml` configs for multiple Relays and monitors frpc processes |
| `internal/heartbeat/` | Heartbeat communications with control planes, including token checks |
| `internal/httpclient/` | General API client for Server communication |
| `internal/sync/` | Incrementally pulls latest Tunnel route bindings, generates snapshots, and applies them |
| `internal/updater/` | Client self-update, new version check, and upgrade installation |
| `internal/wsclient/` | Bi-directional WebSocket client for real-time tunnel configuration pushes |
+130
View File
@@ -0,0 +1,130 @@
# Intranet Penetration Tunnel Design Document
You will learn: The architectural design of the OpenFlare intranet penetration tunnel, the internal principles of the dual-ended control components (Relay and Client), their interaction logics, and the communication flows for the data plane and control plane.
---
## Requirements Analysis
In typical web application hosting scenarios, many origin servers (Origin Servers) are deployed in local intranet environments (such as local development machines, LAN servers, or firewalled private clusters). These servers typically suffer from:
1. **No Public IP**: Cannot be directly accessed by public internet traffic.
2. **Security Compliance Restrictions**: Creating port mappings (NAT) on border routers is strictly prohibited by security policies.
3. **Dynamic IP Changes**: Traditional DDNS solutions exhibit high latency and are highly unstable.
To allow internal origin servers to seamlessly integrate into the OpenFlare global data gateway, benefiting from premium features like WAF geographic protection and TLS certificate hosting, OpenFlare designed an end-to-end solution based on a **reverse relay penetration tunnel**. In this architecture, public edge nodes act as reverse proxy entrances and traffic relays, while the intranet side only needs to initiate secure outbound connections to achieve secure and stable reverse penetration of public traffic to internal origin servers.
---
## Core Capabilities
The intranet penetration tunnel subsystem includes the following core capabilities:
* **Dynamic Relay Node Management**: The control plane dynamically dispatches relay services (frps), distributing service ports and authentication tokens dynamically.
* **Multi-Tunnel Reverse Proxy Mapping**: Supports mapping multiple internal web ports on a single intranet client, binding multiple domain routes to corresponding relay nodes.
* **Independent Process Lifecycle Control**: Both the relay and client are independent daemon processes written in Go, responsible for spawning, monitoring, self-healing, and hot-upgrading the underlying frp engine.
* **Token-based Independent Authentication**: The relay uses `agent_token` for authorization, whereas the intranet client uses its dedicated `tunnel_token`, enforcing isolation of permissions and routing boundaries.
* **Validation & Incremental Hot Reload**: Config files are rewritten and processes are gracefully reloaded only when tunnel bindings, certificates, or Relay topologies change, reducing runtime overhead.
---
## Intranet Penetration & Tunnel Architecture
The intranet penetration subsystem is integrated on top of the mature and high-performance `frp` tunnel protocol, divided into the **Control Plane** and the **Data Plane**.
```mermaid
graph TD
%% Data Flow
Browser[1. Browser / Visitor] -->|HTTPS Request| Agent[2. OpenResty / Agent]
Agent -->|Local proxy_pass| RelayFrps[3. OpenFlare Relay / frps]
RelayFrps -->|Encrypted Tunnel Protocol| FlaredFrpc[4. OpenFlared / frpc]
FlaredFrpc -->|Forward Local Request| LocalOrigin[5. Intranet Origin 192.168.x.x]
%% Control Flow & Heartbeats
Server[OpenFlare Server Control Plane] <-->|Relay API / Heartbeat| RelayManager[openflare_relay process]
Server <-->|Client API / Heartbeat| ClientManager[openflared process]
RelayManager -.->|Control Process & Config| RelayFrps
ClientManager -.->|Control Multi-Relay Processes| FlaredFrpc
style Browser fill:#f9f,stroke:#333,stroke-width:2px
style LocalOrigin fill:#9f9,stroke:#333,stroke-width:2px
style Server fill:#f96,stroke:#333,stroke-width:2px
```
* **Control Plane**: The Server maintains the database state. The `openflare_relay` process on relay nodes and the `openflared` process on intranet servers synchronize tunnel configurations via HTTP heartbeats and long-lived WebSocket connections.
* **Data Plane**: Public traffic enters the public edge Agent (OpenResty), where the TLS handshake, HTTPS termination, and WAF filtering are executed. It is then forwarded via `proxy_pass` to the co-located `openflare_relay (frps)` on the loopback address. `frps` encapsulates the HTTP requests into the encrypted TCP tunnel and sends them down to the intranet `openflared (frpc)`. Finally, `frpc` unpacks the requests and forwards them to the actual intranet origin service.
---
## Relay (Server-side) Design
`openflare_relay` is a relay manager deployed on the public edge, running on nodes of type `tunnel_relay`.
### 1. Core Architecture & Logic
* **Process Daemon**: The Relay process embeds the `frps` binary, spawning the `frps -c frps.toml` subprocess via `exec.Command` and using goroutines to asynchronously listen to its exit status. If `frps` exits unexpectedly, it automatically restarts using an exponential backoff policy.
* **Dynamic Configuration Rendering**: Periodically synchronizes status with the control plane via HTTP heartbeats to retrieve the active `RelayConfig`, including:
* `bindPort`: The public control port that frps listens to for incoming intranet frpc connections.
* `vhostHTTPPort`: The virtual host HTTP listening port where the Agent's proxy_pass points.
* `authToken`: The security credential used during the client connection handshake.
* `webServer`: Enables the frps dashboard API, which the Relay queries to collect active tunnel counts and traffic metrics.
* **Status Reporting**: In each heartbeat cycle, the Relay reports the active connections, registered clients, individual proxy tunnel statuses, and Relay version back to the Server.
---
## Openflared (Client-side) Design
`openflared` is the client manager running inside the user's intranet server, authenticated using a dedicated `tunnel_token`.
### 1. Core Design Mechanisms
* **Multi-Relay Support (Multiplexing)**:
To guarantee high availability and geographical proximity, the control plane may schedule the client to connect to multiple public Relays. `openflared` parses the list of Relays dispatched in the `TunnelConfig`, generating dedicated configurations (`frpc_<relay_node_id>.toml`) and allocating distinct cancelable contexts for each Relay process locally.
* **Independent Subprocess Monitoring**:
`openflared` maintains a local `processes` map to manage the lifecycles of individual `frpc` subprocesses. When the control plane adds or removes Relays, the client incrementally spawns new processes or gracefully shuts down obsolete ones without affecting other functioning tunnels.
* **Dynamic TOML Generation**:
When rendering TOML configs for each Relay, the client iterates over the Proxies list, writing each intranet service's `LocalAddr`, `LocalPort`, and bound `CustomDomains` into standard `[[proxies]]` blocks.
---
## Interaction Logic & Traffic Model
The intranet penetration subsystem implements consistent version control and status feedback loops.
### 1. Control Plane Publishing & Sync Flow
```text
Admin modifies tunnel/intranet port mappings -> Click Publish -> Generate new Tunnel version & Checksum
|
v (Push or Heartbeat Pull)
+-----------------------------------------------------------------------+-----------------------------------------------------------------------+
| |
v (Relay Side) v (Client Side)
openflare_relay heartbeat detects frps port/Token change openflared heartbeat detects tunnel_version change
Re-render local frps.toml Request full proxy configuration details
Kill and restart the frps process Re-render frpc_<relay_id>.toml configs
Report health status as healthy Restart or hot-reload changed frpc processes
Report application results (Apply Success/Error)
```
1. **Versioned Controls**: All intranet tunnel routes and mapping relationships are version-controlled, dispatching a unique `version` and `checksum` to ensure clients do not repeatedly write files or trigger redundant reloads.
2. **Closed-Loop Application Feedback**: After applying new configurations, the client reports the application result in the next heartbeat. If the intranet port is unreachable or certificate bindings fail, the client intercepts the stdout/stderr of the subprocess to report `LastError` to the Server, providing administrators with transparent error details.
### 2. Data Plane Traffic Model
1. **Public Entrance (Agent)**:
```nginx
server {
listen 443 ssl;
server_name intranet.example.com;
# ... TLS certificates & WAF filtering ...
location / {
proxy_pass http://127.0.0.1:18080; # Points to local frps vhost port
proxy_set_header Host $host; # Must preserve the original Host header, which frps relies on to route requests
proxy_set_header X-Real-IP $remote_addr;
}
}
```
2. **Relay Node (frps)**:
`frps` listens to the Vhost port `18080`. When an HTTP request arrives, it extracts `Host: intranet.example.com` from the request headers and searches its active registered tunnel registry to locate the matching encrypted TCP connection (initiated by the intranet frpc).
3. **Encrypted Tunnel Transmission (TCP)**:
`frps` encapsulates the HTTP request into the custom TCP tunnel protocol and transmits it down to the intranet `frpc` client.
4. **Intranet Client Distribution (frpc)**:
The `frpc` instance managed by `openflared` receives the payload, resolves it according to local settings (`localIP = "127.0.0.1"`, `localPort = 8080`), initiates a local TCP connection to forward the request to the intranet web service, and returns the response back through the tunnel to the public viewer.
+132
View File
@@ -0,0 +1,132 @@
# WAF Design Document
You will learn: The core architecture of the OpenFlare edge Web Application Firewall (WAF), the dynamic IP group asynchronous differential sync model, the high-performance OpenResty Lua caching scheme, and the complete request filtering and decision logic.
---
## Requirements Analysis
In public internet environments, web applications face a wide variety of security threats (such as scanner profiling, api scraping, malicious botnets targeted at specific regions, ransomware, and CC attacks). Allowing malicious requests to pass directly to the origin server (Origin Server) results in:
1. **Origin Server Overload**: High-frequency database queries and intensive CPU computations easily exhaust server resources.
2. **Sensitive API Abuse**: APIs like login, registration, and SMS verification codes can be maliciously exploited, leading to financial and computational losses.
3. **Data Exposure Risks**: Malicious common vulnerability probing actions are not intercepted proactively.
Therefore, OpenFlare needs to build a **high-performance, resiliently scalable WAF filtering engine** at the frontmost data plane layer (OpenResty). This engine is capable of executing deep filtering on malicious requests at the edge layer closest to users with sub-millisecond overhead. This relieves pressure on origin servers and provides core security capabilities like CC protection (PoW challenge), IP whitelisting/blacklisting, and region-level interception.
---
## Core Capabilities
OpenFlare WAF includes the following core protection dimensions:
* **IP Interception (IP Whitelist/Blacklist)**: Supports filtering by single IP or CIDR block, and aggregating tens of thousands of IPs into IP groups for highly efficient matching.
* **Geographical Whitelist/Blacklist (GeoIP Limit)**: Integrates MaxMind databases to support precise admission controls based on countries and provinces/regions.
* **Custom Interception Responses**: Supports custom block status codes (e.g., 403, 418) and personalized HTML block pages for different filtering rules.
* **Human-Machine Challenge (PoW CC Protection)**: Supports seamless client-side PoW challenges, calculating Hash collisions to prevent automated scripts and botnets from hitting endpoints concurrently.
---
## IP Group Design & Dynamic Asynchronous Sync
IP groups are the core containers for highly efficient IP whitelisting and blacklisting. OpenFlare classifies IP groups into three types based on their update frequencies and source channels:
### 1. IP Group Types
* **Manual**: Manually input by administrators in the control panel. Primarily used for static trusted IPs or long-term blocks.
* **Subscription**: Configured with remote text feeds (one IP/CIDR per line) or standard JSON subscription URLs. Server-side cron jobs periodically fetch and parse the remote subscription sources. Primarily used for integrating open-source threat intelligence feeds, cloud provider IP ranges, etc.
* **Automatic**: **The most resilient dynamic protection channel**. Control plane scanning jobs read access logs from all nodes, performing aggregation and analysis based on configured Expr rules (e.g., "requesting the `/api/login` endpoint over 50 times with a 401 status code in 5 minutes"). Once matched, the source IP is automatically added to a temporary block list for a specified duration.
### 2. Asynchronous Differential Sync Design (No Nginx Reload)
In traditional Nginx WAF designs, IP blacklist updates typically require writing configurations and executing reloads. If malicious IP blocks occur at high frequencies (seconds or minutes), frequent reloads force Nginx to constantly spawn new worker processes and tear down old ones, severely degrading performance.
OpenFlare adopts a **dynamic IP group asynchronous differential sync design**:
```text
WAF IP member updates (Manual/Subscription/Auto-trigger)
|
v
Server updates the database and calculates the new MD5 Checksum of the IP group
|
+----------------------------------------+
| (WebSocket Real-time Broadcast) | (Heartbeat Fallback Comparison)
v v
Server immediately pushes complete members Agent heartbeats report the local IP groups
of modified groups to all Agents checksum mapping table
| |
| v
| Server detects Checksum mismatch and dispatches
v the modified IP group members
Agent receives member data and writes it as JSON to local disk: waf_ip_groups.json
|
v (Lua Memory Awareness)
OpenResty Lua engine detects file changes via MD5 checksum in seconds and hot-updates its memory,
completely bypassing Nginx process reloads.
```
Through this architecture, the persistence and activation of tens of thousands of highly volatile dynamic blacklist IPs **require absolutely no Nginx reloads**, maximally protecting the high-concurrency throughput of the gateway.
---
## Rule Groups & Site Bindings
* **WAF Rule Group**: The smallest logical collection of WAF filtering policies. A single rule group can contain IP whitelists/blacklists, IP group references, regional restrictions, and CC protection configurations.
* **Global Rule Group**: When a rule group is marked as `is_global = true`, it takes effect on **all website routes** hosted on the node by default.
* **Site Binding**: Website routes (`proxy_routes`) can bind one or more non-global rule groups. During request validation, WAF evaluates the union of `Global Rule Group + Bound Rule Groups`.
---
## Implementation Details & High-Performance Caching
WAF is triggered in the OpenResty `access_by_lua` phase, implemented primarily through Lua files and local JSON configurations.
### 1. Physical Structures
* `waf_config.json`: Contains metadata for all rule groups, geographic country/region limits, and website-to-rule-group bindings.
* `waf_ip_groups.json`: Contains all synchronized IP groups and their corresponding IP lists.
* `waf/runtime.lua`: The actual runtime engine responsible for WAF rule comparison.
* `waf/check.lua`: The entry point for the access layer, handling packages inclusion and triggering `check()`.
### 2. Shared Memory Dictionary (ngx.shared) High-Performance Cache Design
Reading JSON files from the disk and decoding them upon every incoming web request would make disk I/O a severe performance bottleneck.
OpenFlare leverages the **OpenResty Shared Memory Dictionary (ngx.shared.openflare_waf_config)** to implement a two-level caching mechanism:
1. **Zero File I/O Path**:
In Lua, every time `check()` executes, it first computes the MD5 hash of the local JSON file using `ngx.md5` (which takes virtually zero time since the file is cached in the OS Page Cache).
2. **Hash Comparison & Hot Loading**:
It compares this against the cached hash key (`_config_hash`) stored in the shared memory dictionary.
* **If the hash is unchanged**: It reads the pre-decoded Lua Table configuration stored directly in shared memory. The entire verification runs purely in **shared memory**, completing in **microseconds**.
* **If the hash is mismatched**: Indicating that the Agent has just updated the WAF rules or IP groups on the disk, the Lua engine automatically reads the disk file, decodes it via `cjson.decode`, writes the decoded data and the new MD5 hash into shared memory, and makes it seamlessly readable by all subsequent worker processes.
---
## Application Flow & Decision Judgment Control Logic
When an HTTP/HTTPS request arrives at OpenResty, WAF evaluates and intercepts it step-by-step in the `access` phase according to the funnel decision chain below:
### 1. WAF Decision Flowchart
```mermaid
flowchart TD
A[Request enters access phase] --> B[Get Site Name of current request]
B --> C[Load all active rule groups bound to this Site in shared memory]
C --> D{Matches IP whitelist or Whitelist IP group?}
D -- Yes (Matched) --> E[Pass request - ALLOW]
D -- No --> F{Matches country/region whitelist?}
F -- Yes (Matched) --> E
F -- No --> G{Matches IP blacklist or Blacklist IP group?}
G -- Yes (Matched) --> H[Block request - BLOCK]
G -- No --> I{Matches country/region blacklist?}
I -- Yes (Matched) --> H
I -- No --> J{Is CC PoW verification enabled?}
J -- Yes --> K[Transfer to CC Protection module]
J -- No --> L[No security risks, pass normally]
H --> M[Exit and return custom status code and block page HTML configured in the rule group]
```
### 2. Decision Step Details
1. **Whitelist Precedence**:
To prevent false positives and guarantee smooth passage of core back-to-source traffic (such as search engine spiders, CDN back-to-source IPs, and office egresses), WAF **prioritizes matching IP whitelists and regional whitelists**. Once a whitelist matches, it immediately bypasses all subsequent blacklist checks and CC challenges.
2. **Blacklist Aggressive Block**:
If a request is not captured by the whitelist evaluation, it enters the blacklist funnel. Once the source IP matches an IP blacklist, a referenced blacklist IP group, or lies within a prohibited country/region, the Lua engine immediately marks `ngx.ctx.openflare_waf_blocked` as `true`.
3. **Response Output**:
Upon hitting the blacklist, Lua extracts the `block_status_code` (defaults to 418 or 403) and `block_response_body` (interception HTML page) configured in the matching rule group. It outputs the response body via `ngx.say()` and gracefully terminates the request using `ngx.exit(status)` to prevent the request from passing upstream.
-73
View File
@@ -1,73 +0,0 @@
# Connect Agent
OpenFlare Agent runs on proxy nodes. It handles registration, heartbeat, configuration sync, OpenResty file writes, validation, reload, rollback, and self-update.
## Authentication
| Method | Use case |
| --- | --- |
| `agent_token` | The node already exists or has a dedicated credential |
| `discovery_token` | First-time auto-registration; Server exchanges it for a node token |
At least one of them is required.
## Install Script
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--agent-token YOUR_AGENT_TOKEN
```
Or with discovery:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--discovery-token YOUR_DISCOVERY_TOKEN
```
## Configuration Example
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token",
"data_dir": "./data",
"openresty_path": "openresty",
"openresty_observability_port": 18081,
"observability_replay_minutes": 15,
"heartbeat_interval": 10000,
"request_timeout": 10000
}
```
Without `openresty_path`, Agent runs `openresty` by default.
Agent self-update requires the GitHub Release to include both the target binary and a matching `.sha256` file. The downloaded binary is verified before it replaces the local executable.
## Docker
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
```
## Run from Source
```bash
cd openflare_agent
export LOG_LEVEL='info'
go run ./cmd/agent -config /path/to/agent.json
```
## Uninstall
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/uninstall-agent.sh | bash
```
+27
View File
@@ -0,0 +1,27 @@
# Credits
OpenFlare is essentially a solution integration project. During its design and implementation phases, it drew inspiration from the exceptional concepts, architectural designs, and technical achievements of numerous open-source projects. Below are the key upstream open-source projects OpenFlare relies on for its core engine, security mechanisms, and backend/frontend system frameworks, along with our sincere thanks to these projects and their active communities.
---
### 1. OpenResty
* **Project Positioning**: A high-performance Web platform based on Nginx and Lua.
* **Role in OpenFlare**: Acts as the edge gateway for the global Data Plane. All public web traffic is received by OpenResty first, where high-concurrency HTTPS handshakes, WAF security rule evaluations, and PoW CC verification are performed before executing reverse proxies.
* **Project Link**: [OpenResty Official Website](https://openresty.org/)
### 2. FRP (Fast Reverse Proxy)
* **Project Positioning**: A high-performance reverse proxy application focused on intranet penetration.
* **Role in OpenFlare**: Serves as the underlying tunnel engine for the intranet penetration subsystem. The relay-side manager `openflare-relay` is responsible for running and scheduling the `frps` engine, while the intranet client `openflared` is responsible for generating TOML configurations locally and running the multiplexed `frpc` subprocesses.
* **Project Link**: [fatedier/frp (GitHub)](https://github.com/fatedier/frp)
---
### 3. Anubis (PoW Solution)
* **Project Positioning**: A lightweight human-machine verification and protection solution based on Proof of Work (PoW).
* **Role in OpenFlare**: Provides the core **seamless PoW CC challenge** capabilities for the gateway WAF.
---
### 4. gin-template
* **Project Positioning**: A modern full-stack development boilerplate based on Go Gin and frontend builds.
* **Role in OpenFlare**: Provided the standard, unified backend/frontend system architecture baseline for the OpenFlare control plane (Server).
-299
View File
@@ -1,299 +0,0 @@
# Deployment
You will learn: The recommended OpenFlare deployment model, Server and Agent requirements, source startup workflow, integration steps, upgrade paths, and uninstall entry points.
For production, use PostgreSQL for the Server database and set `SESSION_SECRET` explicitly. The recommended deployment method for the Agent is Docker deployment (i.e., running the Agent image that already includes OpenResty); it also supports shell-script installation or running manually.
## Topology
```text
Browser
|
v
OpenFlare Server :3000
|
| Agent API / heartbeat / config pull
v
OpenFlare Agent
|
v
OpenResty binary
|
v
Origin service
```
## Requirements
Server:
| Item | Requirement |
| --- | --- |
| Go | `1.25+`, source run only |
| Node.js | `18+`, frontend source build only |
| Database | Writable SQLite directory or reachable PostgreSQL instance |
| Port | `3000` by default |
Agent:
| Item | Requirement |
| --- | --- |
| OS | Install script supports Linux and macOS. systemd service is created only on Linux + systemd. |
| Architecture | `amd64` or `arm64` |
| OpenResty | Required for local Agent installs, or specified via `--openresty-path` |
| Docker | Required only when running the Agent Docker image |
| Network | Agent node must reach the Server URL |
| GeoIP | WAF regional rules use local MaxMind mmdb; Agent initializes a built-in library and updates it periodically |
[Needs confirmation: recommended production CPU, memory, and disk size]
## Docker Compose Server
Create `docker-compose.yml`:
```yaml
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: openflare
POSTGRES_USER: openflare
POSTGRES_PASSWORD: replace-with-strong-password
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openflare -d openflare"]
interval: 10s
timeout: 5s
retries: 5
openflare:
image: ghcr.io/rain-kl/openflare:latest
container_name: openflare
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "3000:3000"
environment:
SESSION_SECRET: replace-with-a-long-random-string
DSN: postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable
GIN_MODE: release
LOG_LEVEL: info
volumes:
- openflare-data:/data
volumes:
postgres-data:
openflare-data:
```
Start:
```bash
docker compose up -d
docker compose ps
docker compose logs -f openflare
```
Open `http://localhost:3000`. The default account is `root` / `123456`; change it immediately.
## Run Server from Source
Build the management UI first:
```bash
cd openflare_server/web
corepack enable
pnpm install
pnpm build
```
Then start Server:
```bash
cd openflare_server
export SESSION_SECRET='replace-with-a-long-random-string'
export SQLITE_PATH='./openflare.db'
export LOG_LEVEL='info'
# Optional: PostgreSQL takes precedence when set.
# export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable'
go run .
```
Default port is `3000`. You can also set it explicitly:
```bash
go run . --port 3000 --log-dir ./logs
```
## Run Agent in Docker (Recommended)
Docker deployment is the recommended deployment method for the Agent. In Docker deployments, directly run the Agent image. This image is built on top of the OpenResty image and includes both the Agent controller and the OpenResty binary. When `node_ip` is not explicitly configured, the Agent prioritizes obtaining the real public egress IP via a third-party API, avoiding registering the Docker bridge address as the node IP.
Mounting the configuration file:
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-v openflare-agent-data:/data \
-v ./agent.json:/etc/openflare/agent.json:ro \
ghcr.io/rain-kl/openflare-agent:latest
```
Using environment variables:
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
```
## Connect Agent (Script Installation)
In addition to Docker deployment, you can also deploy the Agent on the local host using our installation script.
With `discovery_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--discovery-token YOUR_DISCOVERY_TOKEN
```
With node-specific `agent_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--agent-token YOUR_AGENT_TOKEN
```
Supported options:
| Option | Description |
| --- | --- |
| `--server-url` | Server URL, required |
| `--discovery-token` | First-registration token, mutually exclusive with `--agent-token` |
| `--agent-token` | Node-specific token, mutually exclusive with `--discovery-token` |
| `--install-dir` | Install directory, default `/opt/openflare-agent` |
| `--openresty-path` | OpenResty binary path, auto-detected when omitted |
| `--repo` | GitHub repository for Agent downloads, default `Rain-kl/OpenFlare` |
| `--no-service` | Do not create a systemd service |
Check status:
```bash
systemctl status openflare-agent
journalctl -u openflare-agent -f
```
## Run Agent Manually
From source:
```bash
cd openflare_agent
export LOG_LEVEL='info'
go run ./cmd/agent -config /path/to/agent.json
```
Build and run:
```bash
cd openflare_agent
go build -o openflare-agent ./cmd/agent
export LOG_LEVEL='info'
./openflare-agent -config /path/to/agent.json
```
Minimal `agent.json`:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token",
"data_dir": "./data",
"openresty_path": "openresty",
"heartbeat_interval": 10000,
"request_timeout": 10000
}
```
When `openresty_path` is not configured, Agent runs `openresty`.
By default, the Agent will attempt to upgrade to a WebSocket after a successful HTTP heartbeat. When the upgrade succeeds, the Server immediately notifies the Agent of any configuration publications or activations; if the WebSocket cannot be established or is unexpectedly disconnected, the Agent automatically falls back to HTTP heartbeat synchronization.
WAF regional rules rely on the Agent's local `GeoLite2-Country.mmdb`. Upon startup, the Agent initializes a built-in database at `data_dir/etc/openflare/GeoLite2-Country.mmdb` and attempts to update it periodically based on configuration; update failures only record warnings, and do not affect configuration synchronization or OpenResty reload.
## Minimal Integration Flow
1. Start Server and sign in.
2. Prepare `agent_token` or `discovery_token`.
3. Start Agent and confirm the node is online.
4. Create an enabled site configuration.
5. Publish and activate a new version.
6. Check node detail and apply logs.
7. Visit the domain or verify with `curl`.
## Upgrade and Uninstall
Server:
* Root users can check and upgrade stable Server releases from the top bar.
* Preview releases can be checked manually.
* Binary upload upgrades are also supported.
Agent:
* Agents follow stable releases by default.
* Agent autoupdate requires the GitHub Release to include both the target binary and a matching `.sha256` checksum file; the download must pass SHA-256 validation before the local executable is replaced.
* The install script can be rerun to reinstall or upgrade.
* Preview upgrades require manual action.
Uninstall Agent:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/uninstall-agent.sh | bash
```
The uninstall script stops Agent and removes the systemd service and install directory. It does not remove the local OpenResty installation.
## Validation Commands
Server:
```bash
cd openflare_server
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Agent:
```bash
cd openflare_agent
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Frontend:
```bash
cd openflare_server/web
pnpm build
```
Swagger:
```bash
go install github.com/swaggo/swag/cmd/swag@v1.16.4
cd openflare_server
swag init -g main.go -o docs
```
-188
View File
@@ -1,188 +0,0 @@
# Local Development
You will learn how to set up a local OpenFlare development environment, run the Server, Agent, and frontend, execute tests and builds, and understand the boundaries contributors must follow.
This page is for contributors. Product boundaries, data model constraints, API conventions, and frontend layering are defined in [Development Constraints](../design/development.md). This page focuses on executable local workflows.
## Repository Layout
| Path | Responsibility |
| --- | --- |
| `openflare_server` | Gin + GORM + SQLite/PostgreSQL monolithic control plane |
| `openflare_server/web` | Next.js management UI, statically exported and served by the Go Server |
| `openflare_agent` | Go Agent binary running on nodes |
| `scripts` | Agent install and uninstall scripts |
| `docs` | VitePress documentation site |
## Requirements
| Tool | Requirement |
| --- | --- |
| Go | `1.25+` |
| Node.js | `18+` |
| pnpm | Use `corepack enable` to follow the project-declared version |
| Docker | Needed for Server containers, local integration, and the Agent Docker image |
| OpenResty | Needed when running Agent locally |
| PostgreSQL | Optional. The Server uses SQLite when PostgreSQL is not configured. |
## Install Frontend Dependencies
```bash
cd openflare_server/web
corepack enable
pnpm install
```
Build static assets served by the Go Server:
```bash
pnpm build
```
## Run the Server
SQLite:
```bash
cd openflare_server
export SESSION_SECRET='dev-session-secret'
export SQLITE_PATH='./openflare-dev.db'
export LOG_LEVEL='debug'
go run .
```
PostgreSQL:
```bash
cd openflare_server
export SESSION_SECRET='dev-session-secret'
export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable'
export LOG_LEVEL='debug'
go run .
```
Default URL:
```text
http://localhost:3000
```
Default account: `root` / `123456`.
## Run the Frontend Dev Server
The frontend dev server listens on `3001` by default and proxies API requests through `NEXT_DEV_BACKEND_URL`:
```bash
cd openflare_server/web
export NEXT_DEV_BACKEND_URL='http://127.0.0.1:3000'
pnpm dev
```
Open:
```text
http://localhost:3001
```
## Run the Agent
Create a local `agent.json`:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token",
"data_dir": "./data",
"heartbeat_interval": 10000,
"request_timeout": 10000
}
```
Run:
```bash
cd openflare_agent
export LOG_LEVEL='debug'
go run ./cmd/agent -config ./agent.json
```
When `openresty_path` is not configured, the Agent runs `openresty`. For debugging, set `openresty_path`, `main_config_path`, `route_config_path`, `access_log_path`, `cert_dir`, `lua_dir`, and `runtime_config_dir` as needed.
## Tests
Server:
```bash
cd openflare_server
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Agent:
```bash
cd openflare_agent
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Frontend:
```bash
cd openflare_server/web
pnpm lint
pnpm typecheck
pnpm test
pnpm test:e2e
```
Docs:
```bash
cd docs
pnpm build
```
## Builds
Frontend static assets:
```bash
cd openflare_server/web
pnpm build
```
Server binary:
```bash
cd openflare_server
go build -o openflare-server .
```
Agent binary:
```bash
cd openflare_agent
go build -o openflare-agent ./cmd/agent
```
## Debugging Entrypoints
| Scenario | Command or Location |
| --- | --- |
| Server logs | `LOG_LEVEL=debug go run .` |
| Agent logs | `LOG_LEVEL=debug go run ./cmd/agent -config ./agent.json` |
| Swagger | `http://localhost:3000/swagger/index.html` |
| Frontend API proxy | `NEXT_DEV_BACKEND_URL=http://127.0.0.1:3000 pnpm dev` |
| OpenResty config test | `openresty -t -c ./data/etc/nginx/nginx.conf` |
## Change Acceptance
Before contributing, confirm that:
1. The change fits [Product Boundary](../design/index.md).
2. The implementation follows [Development Constraints](../design/development.md).
3. It does not break release, sync, rollback, or upgrade flows.
4. Documentation is updated when configuration, deployment, API, or product boundaries change.
5. Risky changes include tests or equivalent integration verification.
Database schema changes must bump the database version and include explicit migration and validation logic from the previous version.
+43 -43
View File
@@ -1,88 +1,88 @@
# Publishing Your First Configuration
# Publishing Your First Site
You will learn: How to create your first site configuration, bind origins and certificates, publish a configuration version, and confirm that the Agent has applied it.
You will learn: How to create your first website configuration, bind origins and certificates, publish the configuration version, and verify that the Agent applied it successfully.
OpenFlare's release link is centered on complete configuration versions. After modifying site configurations on the management console, you need to publish and activate the new version before the Agent pulls and applies it in subsequent heartbeats.
The publishing pipeline of OpenFlare centers on a complete configuration version snapshot. After modifying website configurations in the management console, you need to publish and activate the new version to let the Agent pull and apply it in the next heartbeat.
## Pre-release Check
## Pre-publish Checks
Confirm that the following conditions are met:
Verify that the following conditions are met:
| Project | Expectation |
| Item | Expectation |
| --- | --- |
| Server | Can log into the management console |
| Server | Management console is accessible and log-in succeeds |
| Agent | At least one node is online |
| Origin | The Agent node can access the origin address |
| Domain | The domain has been resolved to the OpenResty node, or you are ready to verify via local hosts / curl Host header |
| HTTPS | If HTTPS is required, the certificate has been uploaded or hosted |
| Origin | The Agent node can reach the origin server address |
| Domain | Domain is resolved to the OpenResty node, or prepared to verify via local `hosts` / `curl` Host header |
| HTTPS | If HTTPS is required, the certificate is uploaded or hosted |
## Create Site Configuration
## Create Website Configuration
When adding a site configuration on the management console, you need at least:
A new website configuration requires at least:
| Field | Description |
| --- | --- |
| Site Name | Unique business identifier; defaults to the primary domain when omitted |
| Domains | At least one domain; the first item is treated as the primary domain |
| Origin URL | Valid `http://` or `https://` upstream address |
| Enabled | Only enabled site configurations participate in release rendering |
| Website Name | Business unique identifier; the primary domain is used if left blank |
| Domain | At least one domain, where the first is treated as the primary domain |
| Origin Address | A valid `http://` or `https://` upstream address |
| Enabled Status | Only enabled website configurations will participate in publishing and rendering |
Example:
| Field | Example |
| --- | --- |
| Site Name | `app` |
| Domains | `app.example.com` |
| Origin URL | `http://10.0.0.20:8080` |
| Website Name | `app` |
| Domain | `app.example.com` |
| Origin Address | `http://10.0.0.20:8080` |
A domain can belong to only one site configuration. Site-level rate limits, reverse proxies, and cache configurations are shared by site.
A single domain can belong to only one website configuration. Rate limiting, reverse proxy, and caching parameters are shared site-wide.
## Bind Certificates
## Bind Certificate
HTTPS certificates are bound per domain. Domains not bound to certificates will not be automatically placed in `443 ssl` server blocks.
HTTPS certificates are bound by domain. Domains without a bound certificate will not be placed into `443 ssl` server blocks automatically.
If a site contains multiple domains, the publishing rendering will generate HTTPS configurations grouped by certificate and ensure all domains still belong to the same site snapshot.
If a website contains multiple domains, the rendering pipeline groups the HTTPS configurations by certificate while ensuring all domains belong to the same site snapshot.
## Publish and Activate
## Publish & Activate
Standard link:
Standard Pipeline:
```text
Modify rules -> Preview / View diff -> Publish -> Generate complete configuration version -> Activate version -> Agent pulls -> Local application -> Report result
Modify rules -> Preview / Diff -> Publish -> Generate complete version -> Activate version -> Agent pulls -> Local application -> Report result
```
When publishing, the Server reads all enabled site configurations, OpenResty main configuration templates, performance parameters, and cache parameters, renders the complete OpenResty configuration, calculates the `checksum`, writes to `config_versions`, and then switches the activated version.
During publication, the Server reads all enabled website configurations, the main OpenResty config templates, performance and cache parameters, rendering the complete OpenResty configuration and calculating its `checksum`, saving to `config_versions`, and switching the active version.
## Verify Results
After publishing, confirm on the management console:
Verify in the management console after publishing:
| Location | Expected Result |
| Position | Expected Result |
| --- | --- |
| Node List | Node is online |
| Node Details | The current version is consistent with the activated version |
| Apply Logs | The most recent application succeeded |
| Version Page | The new version is in the activated state |
| Node List | Node status is online |
| Node Details | Current version matches active version |
| Apply Logs | Most recent application succeeded |
| Version Page | The new version is currently active |
Confirm the Agent logs on the node:
Verify Agent logs on the node:
```bash
journalctl -u openflare-agent -n 100 --no-pager
```
Access using the domain:
Access via domain:
```bash
curl -I http://app.example.com
```
If the domain has not been officially resolved yet, you can temporarily specify the Host header to access the node IP:
If the domain has not been officially resolved, you can verify by specifying the Host header against the node IP:
```bash
curl -I -H 'Host: app.example.com' http://NODE_IP
```
HTTPS verification:
HTTPS Validation:
```bash
curl -I https://app.example.com
@@ -90,11 +90,11 @@ curl -I https://app.example.com
## Rollback
If the target version application fails and rolls back, the Agent will block repeated applications of the same `version + checksum` locally until the activated version or checksum on the control plane changes.
If a target version application fails and triggers a rollback, the Agent blocks repeated synchronization of the same failing `version + checksum` until the active version or checksum changes on the control plane.
To roll back to an older version:
Roll back to an older version:
1. Open the configuration version page.
2. Find the previous confirmed working historical version.
3. Reactivate that version.
4. View the node application records to confirm that the Agent applied it successfully.
1. Open the Configuration Versions page.
2. Locate the last known good historic version.
3. Re-activate that version.
4. Check the node application logs to verify that the Agent successfully applied the rollback.
+31 -24
View File
@@ -1,36 +1,43 @@
# Guide
# Guide Overview
You will learn how the OpenFlare documentation is organized, which pages to read for a first run, and where to find deployment, usage, troubleshooting, and development information.
You will learn: How the OpenFlare documentation is organized, which pages to read when running it for the first time, and where to start for deployment, usage, troubleshooting, and development.
OpenFlare is a self-hosted OpenResty control plane. It brings reverse proxy site configuration, immutable releases, Agent-based node sync, TLS certificates, and basic observability into one management UI for a single team or organization.
OpenFlare is a self-hosted OpenResty control plane. It integrates reverse proxy website configurations, configuration version publishing, Agent node synchronization, TLS certificates, and basic observability into a single management console, making it ideal for a single team or organization managing multiple proxy nodes.
## Recommended Path
## Recommended Reading Path
If you are new to OpenFlare, read these pages in order:
If you are new to OpenFlare, read the documents in the following order:
1. [Quick Start](./quick-start.md): start the Server with Docker Compose, sign in, and connect the first Agent.
2. [Usage](./usage.md): learn common operations for sites, origins, certificates, releases, rollbacks, and observability.
3. [Deployment](./deployment.md): run the Server and Agent in an environment closer to production.
4. [Configuration](../reference/configuration.md): look up Server environment variables, runtime options, and Agent configuration fields.
5. [Troubleshooting](./troubleshooting.md): debug login, database, node sync, OpenResty apply, and frontend build issues.
1. [Quick Start](./quick-start.md): Start the Server using Docker Compose, log into the management console, and connect your first Agent.
2. [Basic Usage](./usage.md): Learn common operations for website configs, origins, certificates, publishing, rollbacks, and observability.
3. [Tunnel & Intranet Penetration](./tunnel-usage.md): Learn to deploy Relay and Client to achieve secure, public IP-free reverse penetration.
4. [WAF Security Protection](./waf-usage.md): Master IP whitelisting/blacklisting, WAF auto IP group aggregation Expr rules, geographical restrictions, and PoW CC protection.
5. [WAF Auto IP Group Expressions](./waf-ip-group-expr.md): Write auto IP group Expr rules and learn keyword definitions and presets.
6. [Deployment Guide](../deployment/deployment.md): Deploy Server and Agent in closer-to-production environments.
7. [Configurations Reference](../reference/configuration.md): Check Server environment variables, runtime Options, and Agent configurations.
8. [Troubleshooting](./troubleshooting.md): Troubleshoot login, database, node sync, OpenResty application, and frontend build issues.
## Find by Role
## Role-Based Entrypoints
| Goal | Start Here |
| What do you want to do? | Recommended Entrance |
| --- | --- |
| Run the management UI in a few minutes | [Quick Start](./quick-start.md) |
| Publish the first reverse proxy site | [Publish First Site](./first-site.md) |
| Connect or reinstall a node Agent | [Connect Agent](./agent.md) |
| Start the Server from source | [Run Server](./server.md) |
| Configure GitHub or OIDC login | [SSO Login](./sso.md) |
| Upgrade the Server or Agent | [Upgrade and Maintenance](./upgrade.md) |
| Contribute code or fix issues | [Local Development](./development.md) and [Development Constraints](../design/development.md) |
| Understand architecture and releases | [Architecture](../design/architecture.md) and [Release Model](../design/release-model.md) |
| Run the console in under 5 minutes | [Quick Start](./quick-start.md) |
| Publish your first reverse proxy configuration | [Publish First Configuration](./first-site.md) |
| Configure intranet penetration mapping | [Tunnel & Intranet Penetration](./tunnel-usage.md) |
| Configure CC protection & IP group blocking | [WAF Security Protection](./waf-usage.md) |
| Write auto IP group aggregation rules | [WAF Auto IP Group Expressions](./waf-ip-group-expr.md) |
| Connect or reinstall a node Agent | [Access Agent](../deployment/agent.md) |
| Start Server from source code | [Launch Server](../deployment/server.md) |
| Configure GitHub or OIDC SSO | [SSO Login Configuration](./sso.md) |
| Upgrade Server or Agent | [Upgrade & Maintenance](../deployment/upgrade.md) |
| Participate in development or bug fixing | [Local Development](../design/development.md) and [Development Constraints](../../guildline/development-constraints.md) |
| Understand architecture and publishing | [System Architecture](../design/architecture.md) and [Agent & Publish Model](../design/agent-design.md) |
| View open-source references and credits | [Credits](./credits.md) |
## Documentation Areas
## Documentation Partitions
`guide/` is for users and operators. It provides executable steps from installation to daily operations.
`guide/` is oriented toward users and deployers, providing actionable steps from installation to daily operations.
`reference/` collects stable facts, such as configuration fields, commands, API conventions, and repository layout.
`reference/` collects stable facts such as configuration fields, commands, API response structures, and repository layout.
`design/` is for maintainers and contributors. It describes product boundaries, architecture, release model, and engineering constraints. Update the related design page before implementing changes that alter those boundaries.
`design/` is oriented toward maintainers and contributors, describing product boundaries, system architecture, Agent & publishing models, and engineering constraints. Before adding capabilities or changing boundaries, update the corresponding design document first.
+80 -62
View File
@@ -1,31 +1,32 @@
# Quick Start
You will learn how to start OpenFlare Server with Docker Compose, sign in for the first time, connect the first Agent, and verify that a configuration was published to a node.
You will learn: How to start OpenFlare Server using Docker Compose, complete your first login, connect your first Agent, and verify if a configuration has been published to the node.
The minimal OpenFlare setup contains:
The minimum running unit of OpenFlare consists of:
| Component | Responsibility |
| --- | --- |
| Server | Management UI, management API, Agent API, configuration rendering, release publishing, and state storage |
| Agent | Runs on proxy nodes, pulls configuration, writes OpenResty files, validates, and reloads |
| OpenResty | Receives traffic and proxies requests to origins |
| Server | Admin UI, Admin API, Agent API, configuration rendering, version publishing, and state storage. |
| Agent | Runs on the proxy node, pulls configurations, writes files for OpenResty, executes validations, and triggers reloads. |
| OpenResty | Receives actual traffic and reverse proxies it to origin servers. |
Agent controls OpenResty through the OpenResty binary. Local installs need an `openresty` executable on the node; Docker installs can run the Agent image that already includes OpenResty.
The Agent manages the runtime through the OpenResty binary. A local deployment requires the `openresty` executable to be already present on the node; a Docker deployment can directly run the Agent image containing built-in OpenResty.
## Requirements
## Environment Requirements
| Item | Requirement |
| --- | --- |
| Docker / Docker Compose | Used to start Server and PostgreSQL; also used if you run the Agent Docker image |
| OpenResty | Required for local Agent installs unless `--openresty-path` points to a custom binary |
| Reachable ports | Server listens on `3000` by default. Agent nodes must reach the Server URL. |
| Browser | Used to open the management UI |
| Docker / Docker Compose | Used to start Server and PostgreSQL; also used to run the Agent if using the Docker Agent image |
| OpenResty | Required to have the `openresty` executable when installing the Agent locally, or specify its path in the installation script |
| Reachable Ports | The Server listens on port `3000` by default; the Agent node needs to be able to reach the Server address |
| Browser | Used to access the management console |
[Needs confirmation: minimum recommended Docker and Docker Compose versions]
* **Docker**: `20.10.0+`
* **Docker Compose**: `2.0.0+`
## 1. Start Server
## 1. Start the Server
Create `docker-compose.yml` in an empty directory:
Create a `docker-compose.yml` file in an empty directory:
```yaml
services:
@@ -57,58 +58,62 @@ services:
DSN: postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable
GIN_MODE: release
LOG_LEVEL: info
volumes:
- openflare-data:/data
volumes:
postgres-data:
openflare-data:
```
Start:
Start the services:
```bash
docker compose up -d
```
Verify:
Verify that the containers are running:
```bash
docker compose ps
docker compose logs -f openflare
```
When the `openflare` container is running and logs show `server listening`, open:
Once you see `server listening` in the logs and the `openflare` container status is running, access:
```text
http://localhost:3000
```
Default account:
Default credentials:
| Username | Password |
| --- | --- |
| `root` | `123456` |
Change the default password immediately after first login.
Please change the default password immediately after your first login.
## 2. Prepare an Agent Token
## 2. Prepare Agent Token
Agents can connect with either:
The Agent can be connected using one of two types of credentials:
| Credential | Use Case |
| Credential | Applicable Scenario |
| --- | --- |
| `discovery_token` | First-time automatic node registration. Server exchanges it for a node-specific token. |
| `agent_token` | A node-specific token created or assigned in the management UI. |
| `discovery_token` | Automatically registers a node for the first time, which the Server exchanges for a node-specific Token |
| `agent_token` | Node has already been created/allocated in the management console, directly uses this node-specific Token |
Prepare one of them in the management UI before continuing.
After preparing one of these credentials in the management console, proceed to the next step.
[Needs confirmation: exact UI menu path for creating or viewing `discovery_token` and node `agent_token`]
* **`discovery_token`** path: "System Settings" -> "Auto Registration"
* **`agent_token`** path: "Node Management" -> "Add Node"
## 3. Install/Run Agent
## 3. Install/Run the Agent
The recommended deployment method for the Agent is Docker deployment (i.e., running the Agent image that already includes OpenResty); it also supports shell-script installation on the local host.
The recommended Agent deployment method is using Docker (which runs the Agent image with built-in OpenResty); deploying the Agent locally on the host using the installation script is also supported.
### Option A: Run Agent in Docker (Recommended)
Run the Agent Docker image on the proxy node:
Run the Agent image directly on the proxy node:
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
@@ -121,11 +126,11 @@ docker run -d --name openflare-agent --restart unless-stopped \
ghcr.io/rain-kl/openflare-agent:latest
```
### Option B: Run the Installation Script (Local Host)
### Option B: Execute Installation Script (Local Host Deployment)
Run the install script on the proxy node.
Execute the installation script on the proxy node.
With `discovery_token`:
Using the `discovery_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
@@ -133,7 +138,7 @@ curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/inst
--discovery-token YOUR_DISCOVERY_TOKEN
```
With node-specific `agent_token`:
Using the node-specific `agent_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
@@ -143,46 +148,46 @@ curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/inst
The script defaults to:
| Item | Default |
| Item | Default Value |
| --- | --- |
| Install directory | `/opt/openflare-agent` |
| Config file | `/opt/openflare-agent/agent.json` |
| systemd service | `openflare-agent.service` |
| OpenResty path | Auto-detects `openresty` unless `--openresty-path` is provided |
| Install Directory | `/opt/openflare-agent` |
| Config File | `/opt/openflare-agent/agent.json` |
| systemd Service | `openflare-agent.service` |
| OpenResty Path | Automatically detects `openresty` if unspecified |
Check status:
Verify the Agent service status:
```bash
systemctl status openflare-agent
journalctl -u openflare-agent -f
```
If systemd is unavailable, the script prints a manual start command.
If systemd is not available on the OS, the script outputs manual startup commands instead.
## 4. Publish the First Configuration
## 4. Publish Your First Configuration
In the management UI:
Perform the following operations in the management console:
1. Create a site configuration with a site name, domain, and origin URL.
2. Ensure the site is enabled.
3. Preview the rendered configuration or review the diff.
4. Publish and activate a new version.
5. Wait for the Agent to discover and apply the version through heartbeat.
1. Add a website configuration, filling in the website name, domain, and origin address.
2. Verify that the website configuration is enabled.
3. Check the preview or change summary before publishing.
4. Publish and activate the new version.
5. Wait for the Agent to detect and apply the version in the next heartbeat.
Version numbers use `YYYYMMDD-NNN`. Historical versions are immutable; rollback reactivates an old version.
The version number format is `YYYYMMDD-NNN`. Historic versions are immutable; rollbacks are accomplished by re-activating an older version.
## 5. Verify Success
In the UI:
Confirm in the management console:
| Location | Expected Result |
| Position | Expected Result |
| --- | --- |
| Node list | Agent node is online |
| Node detail | Current version matches the active version |
| Apply logs | Latest apply succeeded |
| Versions page | New version is active |
| Node List | Agent node status is online |
| Node Details | Current version matches active version |
| Apply Logs | Most recent application succeeded |
| Version Page | The new version is currently active |
On the Agent node:
Confirm on the Agent node:
```bash
journalctl -u openflare-agent -n 100 --no-pager
@@ -190,12 +195,25 @@ journalctl -u openflare-agent -n 100 --no-pager
## Common Failures
| Symptom | What to Check |
| Symptom | Troubleshooting Direction |
| --- | --- |
| Cannot open the UI | Confirm `docker compose ps` shows Server running and host port `3000` is free |
| Login works but data cannot be saved | Check PostgreSQL health and the username/password/database in `DSN` |
| Agent cannot register | Confirm the Agent node can reach `--server-url`, and check whether the token is wrong or expired |
| Agent is online but does not apply | Confirm the site is enabled and a version was published and activated |
| OpenResty apply fails | Check apply logs and `journalctl -u openflare-agent`, especially domains, certificates, upstream URLs, and port conflicts |
| Management console fails to load in browser | Verify that the Server is running in `docker compose ps` and port `3000` is not bound by other processes |
| Data fails to save after logging in | Check the health of the PostgreSQL container, and verify the username, password, and database name in `DSN` |
| Agent fails to register | Verify that the Agent node can reach `--server-url`, and verify if the Token is typed correctly or expired |
| Agent is online but configuration is not applied | Verify that the website configuration is enabled and a version has been published and activated |
| OpenResty application fails | Review node application logs and `journalctl -u openflare-agent`, checking domains, certificates, upstreams, and port conflicts |
See [Troubleshooting](./troubleshooting.md) for deeper diagnostics.
For more troubleshooting details, see [Troubleshooting](./troubleshooting.md).
---
## Advanced Deployment Guides
Once you complete the quick start and familiarize yourself with the basic operations of OpenFlare, you can read the following advanced deployment documents to put components into production:
* **Server Production Deployment**: Read [Launch Server](../deployment/server.md) to learn how to build the frontend from source, configure system environment variables, and run with Docker Compose.
* **Agent Production Integration**: Read [Deploy Agent](../deployment/agent.md) to learn about systemd-based service management, detailed local configuration parameters, and troubleshooting.
* **Tunnel Relay Deployment**: Read [Deploy Relay](../deployment/relay.md) to learn how to configure public relay nodes (frps) for penetration tunnels.
* **Tunnel Client Deployment**: Read [Deploy OpenFlared](../deployment/openflared.md) to learn how to run the penetration daemon client (frpc) on the intranet server side.
* **Production Deployment Topology**: Read [Deployment Guide](../deployment/deployment.md) to learn about high-availability production topologies and overall network planning.
* **System Upgrades & Maintenance**: Read [Upgrade & Maintenance](../deployment/upgrade.md) to learn how to upgrade the Server and individual node Agents smoothly.
-106
View File
@@ -1,106 +0,0 @@
# Starting the Server
You will learn: How to build the management console frontend from source, start OpenFlare Server, select SQLite or PostgreSQL, and access Swagger.
OpenFlare Server is a Gin + GORM monolithic control plane, responsible for the management console UI, management APIs, Agent APIs, configuration rendering, version releases, data storage, and aggregated queries.
## Prerequisites
| Project | Requirement |
| --- | --- |
| Go | `1.25+` |
| Node.js | `18+` |
| pnpm | Recommended to use the pnpm declared by the project via `corepack enable` |
| Database | SQLite file directory is writable, or an accessible PostgreSQL instance |
In production environments, it is recommended to explicitly configure `SESSION_SECRET` and prioritize PostgreSQL.
## Build the Management Console Frontend
The Go Server hosts the static artifacts in `openflare_server/web/build`. Before starting from source, build the frontend first:
```bash
cd openflare_server/web
corepack enable
pnpm install
pnpm build
```
Common frontend checks:
```bash
pnpm lint
pnpm typecheck
pnpm test
```
## Start with SQLite
```bash
cd openflare_server
export SESSION_SECRET='replace-with-a-long-random-string'
export SQLITE_PATH='./openflare.db'
export LOG_LEVEL='info'
go run .
```
Listens on port `3000` by default. Access:
```text
http://localhost:3000
```
## Start with PostgreSQL
```bash
cd openflare_server
export SESSION_SECRET='replace-with-a-long-random-string'
export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable'
export LOG_LEVEL='info'
go run .
```
`DSN` takes precedence over SQLite once set. When `DSN` and the legacy-named `SQL_DSN` both exist, `DSN` takes precedence.
If the target PostgreSQL database is empty and the local `SQLITE_PATH` file exists, the Server will attempt to migrate SQLite data to PostgreSQL during the startup phase and output the migration progress in the logs.
## Command Line Parameters
```bash
go run . --port 3000 --log-dir ./logs
```
| Parameter | Action | Default Value |
| --- | --- | --- |
| `--port` | Specify the Server listening port | `3000` |
| `--log-dir` | Specify the log directory | Empty (outputs to standard output) |
| `--version` | Output the version and exit | `false` |
| `--help` | Output the help information and exit | `false` |
## First Login
Default account:
| Username | Password |
| --- | --- |
| `root` | `123456` |
Please change the default password immediately after logging in for the first time.
## Swagger
Access after logging into the management console:
```text
http://localhost:3000/swagger/index.html
```
Regenerate Swagger locally:
```bash
go install github.com/swaggo/swag/cmd/swag@v1.16.4
cd openflare_server
swag init -g main.go -o docs
```
The generated Swagger files are located in `openflare_server/docs`.
+56 -54
View File
@@ -1,104 +1,106 @@
# SSO Login
# SSO Login Configuration
You will learn how to configure GitHub OAuth or a standard OIDC login source for OpenFlare, how to set callback URLs, and how third-party accounts bind to local users.
You will learn: How to configure GitHub OAuth or standard OIDC login portals for OpenFlare, how to fill in callback URLs, and how third-party accounts bind to existing local users.
OpenFlare supports third-party login through authentication sources. The current supported source types are GitHub OAuth and standard OIDC providers, such as Logto, authentik, Keycloak, and Casdoor.
OpenFlare supports third-party logins configured via Authentication Sources. Currently, GitHub OAuth and standard OIDC Providers (e.g., Logto, authentik, Keycloak, Casdoor) are supported.
After an authentication source is configured and enabled, it appears on the login page. Users can sign in with the third-party account or bind it to the current local account while already signed in.
Once an Authentication Source is configured and enabled, it displays in the third-party login section of the login page. Users can log in using their third-party accounts or bind their third-party accounts to their current local account while logged in.
## Before You Start
## Prerequisites
Prepare:
Before starting, prepare the following:
| Item | Description |
| --- | --- |
| OpenFlare public URL | The URL users open in their browser, such as `https://openflare.example.com` |
| Source name | Internal unique name, such as `github` or `company-oidc` |
| Client ID | Provided by the third-party application |
| Client Secret | Provided by the third-party application |
| OIDC Discovery URL | Required only for OIDC, such as `https://idp.example.com/.well-known/openid-configuration` |
| OpenFlare URL | The actual URL accessed by user browsers, e.g., `https://openflare.example.com` |
| Auth Source Name | Unique internal identifier in OpenFlare, e.g., `github`, `company-oidc` |
| Client ID | Provided after creating an application in the third-party platform |
| Client Secret | Provided after creating an application in the third-party platform |
| OIDC Discovery URL | Required for OIDC only, e.g., `https://idp.example.com/.well-known/openid-configuration` |
Confirm that the server address in system settings matches the domain users access.
**Verify that "System Settings -> General Settings -> Server Address" accurately matches your domain name.**
The source name can contain letters, numbers, hyphens, and underscores, and must start with a letter or number. The source name is part of the callback URL. If you rename it later, update the callback URL in the third-party platform too.
The Auth Source name can only contain letters, numbers, hyphens, or underscores, and must start with a letter or number. The Auth Source name will appear in the callback URL; if you modify the name after saving, you must simultaneously modify the callback URL on the third-party platform.
## Callback URL
Set the Redirect URI / Callback URL in the third-party platform to:
The Redirect URI / Callback URL in third-party platforms is formatted as:
```text
<OpenFlare public URL>/oauth/<source name>
<OpenFlare URL>/oauth/<Auth Source Name>
```
Examples:
Example:
```text
https://openflare.example.com/oauth/github
https://openflare.example.com/oauth/company-oidc
```
When creating or editing an authentication source, the UI shows the callback URL based on the current browser URL and source name.
When creating or editing an authentication source in the management console, the form automatically generates the callback URL based on your current browser URL and the Auth Source name you entered.
## Configure GitHub Login
1. Create an OAuth App in GitHub.
2. Set `Homepage URL` to the OpenFlare public URL.
3. Set `Authorization callback URL` to the callback shown by OpenFlare, such as `https://openflare.example.com/oauth/github`.
4. Copy the Client ID and Client Secret.
5. Sign in to OpenFlare and open Settings -> System Settings -> Authentication Sources.
6. Add a source and select `GitHub`.
7. Fill in source name, display name, Client ID, and Client Secret.
8. Keep the default scope `user:email` unless your GitHub app requires a different value.
9. Save and enable the source.
2. Fill `Homepage URL` with your OpenFlare URL.
3. Fill `Authorization callback URL` with the callback URL generated in OpenFlare, e.g., `https://openflare.example.com/oauth/github`.
4. Copy the Client ID and Client Secret provided by GitHub.
5. Log into the OpenFlare management console, go to "Settings -> System Settings -> Configure Authentication Sources".
6. Add an authentication source, choosing `GitHub` as the type.
7. Fill in the Auth Source name, display name, Client ID, and Client Secret.
8. The Scope defaults to `user:email`, which usually requires no modification.
9. Save and enable the authentication source.
The login page will show the GitHub button after the source is enabled.
Once enabled, the corresponding GitHub login button will display on the login page.
## Configure OIDC Login
1. Create an application or client in the OIDC provider.
2. Choose a Web / Confidential Client type.
3. Set Redirect URI / Callback URL to the value shown by OpenFlare, such as `https://openflare.example.com/oauth/company-oidc`.
1. Create an application or client in your OIDC Provider.
2. Select Web / Confidential Client as the application type.
3. Fill `Redirect URI / Callback URL` with the callback URL generated in OpenFlare, e.g., `https://openflare.example.com/oauth/company-oidc`.
4. Copy the Client ID and Client Secret.
5. Get the provider Discovery URL, usually ending in `/.well-known/openid-configuration`.
6. Sign in to OpenFlare and open Settings -> System Settings -> Authentication Sources.
7. Add a source and select `OIDC`.
8. Fill in source name, display name, Client ID, Client Secret, and OIDC Discovery URL.
9. Keep the default scope `openid profile email` unless the provider restricts scopes.
10. Save and enable the source.
5. Retrieve the Provider's Discovery URL, which usually ends with `/.well-known/openid-configuration`.
6. Log into the OpenFlare management console, go to "Settings -> System Settings -> Configure Authentication Sources".
7. Add an authentication source, choosing `OIDC` as the type.
8. Fill in the Auth Source name, display name, Client ID, Client Secret, and OIDC Discovery URL.
9. Scope defaults to `openid profile email`. If the Provider restricts scopes, adjust to values permitted by the Provider.
10. Save and enable the authentication source.
The login page will show the OIDC button after the source is enabled.
Once enabled, the corresponding OIDC login button will display on the login page.
## Login and Binding Behavior
## Login & Binding Behaviors
Once a third-party account returns to OpenFlare, it is processed according to the following rules:
| Scenario | Behavior |
| --- | --- |
| Third-party account already bound to a local user | Sign in directly |
| User is already signed in and starts third-party authorization | Bind the third-party account to the current local user |
| Third-party account is unbound and registration is allowed | Create a normal local user and bind it |
| Third-party account is unbound and registration is disabled | Ask the user to enter existing local credentials to bind |
| Third-party account is already bound to a local user | Logs in directly |
| User is already logged in and initiates third-party authorization | Binds to the current local user |
| Third-party account is unbound, and registration is enabled | Automatically creates a standard user and binds |
| Third-party account is unbound, and registration is disabled | Prompts to enter an existing local username and password to complete the binding |
If you only want existing users to use SSO, disable registration. Unbound third-party accounts will enter the existing-account binding flow.
If you want only existing users to use SSO, you can disable user registration. Unbound third-party accounts will then trigger the binding flow.
## Update a Source
## Modify Authentication Source
When editing an authentication source, leave Client Secret empty to keep the existing secret. Entering a new value overwrites it.
When editing an authentication source, leaving the Client Secret field blank retains the existing secret; entering a new value will overwrite the saved secret.
If you change the source name, the callback URL changes too. Update Redirect URI / Callback URL in the third-party platform, or the provider will reject the callback.
If you modify the Auth Source name, the callback URL changes accordingly. You must modify the Redirect URI / Callback URL on the third-party platform; otherwise, the third-party platform will deny the callback or return an error.
## FAQ
## Common Problems
### `invalid_scope`
### Returns `invalid_scope`
The provider does not allow the configured scope. The OIDC default is `openid profile email`; the GitHub default is `user:email`. Adjust the scope in OpenFlare or allow it in the provider.
This indicates that the third-party platform does not permit the configured Scope. OIDC defaults to `openid profile email`, and GitHub defaults to `user:email`. Adjust the Scope in the authentication source edit page or configure the third-party platform to permit the scope.
### Callback URL Mismatch
### Callback Address Mismatch
Check that the Redirect URI / Callback URL in the provider exactly matches the URL shown by OpenFlare. Protocol, domain, port, and path must all match.
Verify if the Redirect URI / Callback URL configured in the third-party platform matches the prompt in the OpenFlare form exactly. The protocol, domain, port, and path must match.
### No Third-Party Login Button
### Third-party Login Button Not Showing on Login Page
Check that the source is enabled and that Client ID and Client Secret are saved. OpenFlare validates these fields before enabling a source.
Verify if the authentication source is enabled and confirm that the Client ID and Client Secret are saved. OpenFlare validates these fields before enabling the source.
### Client Secret Is Not Shown in the List
### Client Secret Saved but Not Displayed in Clear Text
This is expected. OpenFlare does not return Client Secret through the API; it only shows whether the secret is configured.
This is expected behavior. OpenFlare does not echo the Client Secret back via API, displaying only whether the secret is configured.
+110 -86
View File
@@ -1,45 +1,45 @@
# Troubleshooting
You will learn how to debug OpenFlare Server, database, login, Agent, OpenResty, release, and frontend build issues by symptom.
You will learn: How to troubleshoot OpenFlare Server, database, login, Agent, OpenResty, configuration publishing, and frontend build issues by symptoms.
Start by locating the failing layer: browser, Server, database, Agent, OpenResty, origin, or DNS. OpenFlare applies configuration only after a version is activated and the Agent discovers it through heartbeat.
During troubleshooting, first identify which layer the issue occurs in: browser, Server, database, Agent, OpenResty, origin server, or DNS. OpenFlare configurations are not written directly to nodes online; only after the active version changes will the Agent detect and apply it in heartbeats.
## Quick Triage
## Quick Diagnostic
| Symptom | Check First |
| Symptom | Where to check first |
| --- | --- |
| Management UI does not open | Server process/container logs and port binding |
| Login fails | Default account, `SESSION_SECRET`, browser request, Server logs |
| Data cannot be saved | Database connection, SQLite permissions, PostgreSQL health |
| Agent is offline | Agent logs, token, Server URL, network reachability |
| Node does not update after release | Active version, node heartbeat, apply logs |
| OpenResty apply fails | Apply logs, Agent logs, certificates, upstream URL, port conflicts |
| No access analytics | OpenResty status, observability port, Agent replay logs |
| Admin panel fails to open | Server container or process logs, port listening |
| Login anomalies | Default credentials, Session Secret, browser request payloads, Server logs |
| Data fails to save | Database connection, SQLite file permissions, PostgreSQL health |
| Agent offline | Agent logs, Token, Server URL, network connectivity |
| Node not updated after publishing | Active version, node heartbeat, application logs |
| OpenResty application failed | Application logs, Agent logs, certificates, upstream addresses, port conflicts |
| Observability analytics has no data | OpenResty container status, observability port, Agent retry logs |
## Server Does Not Start
## Server Fails to Start
1. Check logs:
1. View logs:
```bash
docker compose logs -n 200 openflare
```
For source runs, check terminal output.
For source-code execution, inspect terminal outputs.
2. Check port usage:
2. Check port conflicts:
```bash
lsof -i :3000
```
3. If PostgreSQL is used, check database health:
3. If using PostgreSQL, verify that the database is healthy:
```bash
docker compose ps postgres
docker compose logs -n 100 postgres
```
4. If SQLite is used, check that the database directory is writable:
4. If using SQLite, verify that the database directory is writable:
```bash
ls -ld "$(dirname /path/to/openflare.db)"
@@ -47,154 +47,178 @@ ls -ld "$(dirname /path/to/openflare.db)"
Common causes:
| Log or Symptom | Fix |
| Log or Symptom | Action |
| --- | --- |
| Database connection failed | Check username, password, host, port, database, and `sslmode` in `DSN` |
| SQLite cannot create file | Check that the `SQLITE_PATH` directory exists and is writable |
| Port is already in use | Change `PORT` or `--port`, or stop the process using the port |
| Database connection failed | Check `DSN` username, password, host, port, dbname, and `sslmode` |
| SQLite fails to create files | Check if the parent directory of `SQLITE_PATH` exists and is writable |
| Port is already in use | Change `PORT` or `--port`, or stop the process binding to the port |
## UI Does Not Open or Is Blank
## Admin Console Fails to Load or Shows Blank Page
1. Confirm that the Server responds:
1. Verify that the Server is listening:
```bash
curl -I http://127.0.0.1:3000
```
2. For source runs, confirm frontend static assets were built:
2. If running from source, verify that the frontend static assets have been built:
```bash
cd openflare_server/web
pnpm build
```
3. Check whether the browser URL matches your reverse proxy setup.
3. Verify if the browser URL matches your reverse proxy domain.
4. If using the frontend dev server, confirm backend proxy configuration:
4. If accessing via the frontend dev server, verify the backend proxy configuration:
```bash
cd openflare_server/web
NEXT_DEV_BACKEND_URL=http://127.0.0.1:3000 pnpm dev
```
## Default Account Cannot Sign In
## Default Credentials Fail to Log In
The default account is `root` / `123456`. If the password was changed after first login, use the updated password.
The default credentials are `root` / `123456`. If you have modified the password after your first login, use your new password.
Steps:
Troubleshooting Steps:
1. Confirm the Server is connected to the expected database, not another `SQLITE_PATH` or `DSN`.
2. Check Server logs to see whether it uses `sqlite` or `postgres`.
3. If deployed behind replicas or a reverse proxy, ensure `SESSION_SECRET` is fixed and consistent across instances.
4. Clear browser cookies and try again.
1. Confirm that you are connecting to the expected database, avoiding `SQLITE_PATH` or `DSN` pointing to a different environment.
2. Check the Server log to see if it is running on `sqlite` or `postgres`.
3. If deployed in multi-replicas or behind a reverse proxy, verify that `SESSION_SECRET` is static and uniform across all instances.
4. Clear browser Cookies and try logging in again.
[Needs confirmation: whether the project provides a safe root password reset command or procedure]
### Emergency Reset of Admin Password
## Agent Cannot Register or Stays Offline
If you forget the password for the `root` account, you can reset it back to `123456` by directly updating the password hash in the database (please change it immediately after logging in):
On the Agent node:
#### 1. If using SQLite Database
Stop the Server and open the database file using the `sqlite3` client:
```bash
sqlite3 /path/to/openflare.db
```
Execute the following SQL statement:
```sql
UPDATE users SET password_hash = '$2a$10$wN9aE3zTz83rO7R1uKlhuehJtA3c604pX4Z12B/9.5c0X337t1L4m' WHERE username = 'root';
```
Type `.exit` to exit and restart the Server.
#### 2. If using PostgreSQL Database
Connect to your PostgreSQL instance using a database tool (e.g., `psql`, `pgAdmin`, or `DBeaver`), select the corresponding `openflare` database, and execute the following SQL:
```sql
UPDATE users SET password_hash = '$2a$10$wN9aE3zTz83rO7R1uKlhuehJtA3c604pX4Z12B/9.5c0X337t1L4m' WHERE username = 'root';
```
Once executed successfully, you can log in using the default password `123456`.
## Agent Fails to Register or Stays Offline
Execute on the Agent node:
```bash
curl -I http://your-server:3000
```
Check Agent logs:
Inspect Agent logs:
```bash
journalctl -u openflare-agent -n 200 --no-pager
```
Check config:
Verify configuration parameters:
```bash
sed -n '1,160p' /opt/openflare-agent/agent.json
```
Confirm:
Key Settings:
| Config | Notes |
| Configuration | Description |
| --- | --- |
| `server_url` | Must be reachable from the Agent node |
| `agent_token` / `discovery_token` | At least one is required |
| `heartbeat_interval` | Supports millisecond integers or Go duration strings |
| `request_timeout` | Increase it for slow networks |
| `server_url` | Must be the Server address reachable by the Agent node |
| `agent_token` / `discovery_token` | At least one must be provided |
| `heartbeat_interval` | Supports integer milliseconds or Go duration strings |
| `request_timeout` | Can be increased for slower network links |
If the log says the token is invalid, prepare a new token in the UI, update `agent.json`, and restart:
If the log warns that the Token is invalid, retrieve a new Token in the management console, update `agent.json`, and restart the Agent:
```bash
systemctl restart openflare-agent
```
## Node Does Not Apply a New Version
## Node Fails to Apply New Version after Publishing
Check in order:
Verify in sequence:
1. The target version is active on the versions page.
2. The node is online and heartbeat time is updating.
3. Apply logs contain a success, warning, or failure for the target version.
4. The site configuration is enabled.
5. Agent logs show pull, validation, reload, or rollback messages.
1. Confirm that the target version is activated on the Versions page.
2. Verify if the node is online and if its last heartbeat time has updated.
3. Check the Application Logs for successful, warned, or failed logs for the target version.
4. Verify if the website configuration is enabled; disabled websites do not participate in rendering.
5. Inspect Agent logs for pulls, validations, reloads, or rollback events.
Follow Agent logs:
Inspect Agent logs:
```bash
journalctl -u openflare-agent -f
```
After a target `version + checksum` fails and rolls back, the Agent blocks repeated attempts for that same target locally. Fix the configuration and publish a new checksum, or activate an old version to roll back.
Note: If a target `version + checksum` fails to apply and triggers a rollback, the Agent blocks repeated synchronization of that failing target in its local state. You must fix the configuration issues and republish to generate a new checksum, or activate an older version to trigger a rollback.
## OpenResty Apply Fails
If this is the Agent's first time applying configurations and no historic `nginx.conf` exists locally to roll back to, the failed version remains blocked but the Agent will attempt to enter the safe fallback runtime. At this point, the application logs and Agent logs will contain `fallback runtime started`. OpenResty will only listen to port `80`, returning a `503` with the body `OpenFlare: No Valid Configuration`, while retaining the local `/openflare/stub_status` health probe. After correcting the configurations and republishing, the Agent overrides the fallback config and restores normal reverse proxies.
Common causes:
## OpenResty Application Fails
| Cause | Check |
Common Causes:
| Cause | Diagnostic |
| --- | --- |
| Domain or server block conflict | Ensure the same domain is not used by multiple sites |
| Invalid upstream URL | Every upstream must be `http://` or `https://` |
| Invalid multi-upstream format | Multiple upstreams must be plain `scheme://host[:port]` |
| Missing certificate or wrong path | Check domain certificate binding and Agent certificate directory permissions |
| Port conflict | Check local `80` and `443` usage |
| Domain or server block conflict | Verify if the same domain is used by multiple website configurations |
| Invalid upstream address | Confirm that all upstreams are valid `http://` or `https://` URLs |
| Mismatched multi-upstream format | Multi-upstreams must be pure `scheme://host[:port]` |
| Missing cert or invalid paths | Verify if domains are bound to certs and check if the Agent cert directory is writable |
| Port already in use | Verify ports `80` and `443` on the host |
OpenResty config test:
OpenResty Configuration Validation:
```bash
openresty -t -c /path/to/openflare/data/etc/nginx/nginx.conf
```
OpenResty runtime:
OpenResty Runtime Status:
```bash
ps aux | grep openresty
```
Agent periodic health checks use local `http://127.0.0.1:<openresty_observability_port>/openflare/stub_status` instead of repeatedly running `openresty -t`. If a node is unhealthy, first confirm that the local observability port is listening. If `host not found in upstream` only appears during apply, the failure comes from config validation or reload, not the periodic health probe.
The Agent determines OpenResty survival periodically using the local endpoint `http://127.0.0.1:<openresty_observability_port>/openflare/stub_status`, completely bypassing repeated `openresty -t` calls. If a node is marked as unhealthy, confirm if this local observability port is listening. If failures only occur when applying configurations (e.g., `host not found in upstream`), the failure lies in config validation or reload, not the periodic health checks.
Use the actual `openresty_path` and `main_config_path` from `agent.json`.
Actual binary paths and main configuration paths are governed by `openresty_path` and `main_config_path` in `agent.json`.
## HTTPS Does Not Work
## HTTPS Fails to Work
1. Confirm the certificate exists.
2. Confirm the domain is bound to that certificate in the site configuration.
3. Confirm a new version was published and activated.
4. Check apply logs for success.
5. Inspect with `curl`:
1. Verify that the certificate has been uploaded or hosted.
2. Verify that the website configuration binds the certificate to the domain.
3. Confirm that the configuration version has been published and activated.
4. Check if the Application Logs indicate a success.
5. Check the certificate chain and status code using `curl`:
```bash
curl -Iv https://your-domain
```
Domains without a bound certificate are not automatically added to HTTPS configuration.
Domains without a bound certificate will not be added to the HTTPS configuration automatically; this is expected behavior.
## No Access Analytics
## Traffic Analytics Has No Data
1. Confirm the node applied a configuration that includes observability Lua assets.
2. Confirm OpenResty is running.
3. Check Agent logs for collection or replay failures.
4. Check whether `openresty_observability_port` is occupied. The default is `18081`.
5. Confirm Server cleanup policy did not remove data for that time window.
1. Confirm that the node has successfully applied configurations carrying observability Lua scripts.
2. Verify that OpenResty is running.
3. Check Agent logs for observability extraction or upload errors.
4. Check if `openresty_observability_port` (default is `18081`) is bound by other processes.
5. Verify if the Server database has purged data inside the time window.
## Frontend Build Fails
Execute:
```bash
cd openflare_server/web
corepack enable
@@ -207,14 +231,14 @@ pnpm build
Common causes:
| Symptom | Fix |
| Symptom | Action |
| --- | --- |
| pnpm version mismatch | Run `corepack enable` and reinstall |
| Type errors | Run `pnpm typecheck` to locate files |
| API type mismatch | Check `lib/api/` and `types/` response structures |
| E2E fails | Ensure both the Server and frontend dev server are running |
| pnpm version mismatch | Reinstall packages after executing `corepack enable` |
| TypeScript errors | Locate detailed file bugs by running `pnpm typecheck` |
| API type mismatch | Check responses structures in `lib/api/` and `types/` |
| E2E test failures | Confirm that both the Server and frontend dev server are running |
## Docs Build Fails
## Documentation Build Fails
```bash
cd docs
@@ -222,4 +246,4 @@ pnpm install
pnpm build
```
If the failure is a link error, check that new pages are added to `docs/en/config.ts` and that relative links point to existing Markdown files.
If it fails on broken links, check if new pages are added to the `docs/config.ts` sidebar, or if relative markdown links point to existing markdown files.
+174
View File
@@ -0,0 +1,174 @@
# Tunnel & Intranet Penetration
You will learn: The design principles of OpenFlare intranet penetration tunnels, core concepts (Relay nodes and Tunnel clients), and how to safely and stably publish your intranet development environment or private cloud services to a public domain name from scratch.
In many practical development and operations scenarios, our origin servers are deployed in local LANs, local development machines, or heavily guarded private VPCs, having no public IP address and no port mapping (NAT) configured on border firewalls or routers.
OpenFlare provides an end-to-end solution **based on reverse relay penetration tunnels**. You only need to initiate a secure outbound connection from your intranet environment to the public relay node, without configuring any inbound ports, to smoothly route public web traffic into your intranet origin. At the same time, you benefit from automatic TLS certificate hosting and WAF security protection provided by the gateway.
---
## Core Concepts
Before using the intranet penetration features, you need to familiarize yourself with the following components and core concepts:
| Concept | Description | Component / Operation |
| --- | --- | --- |
| **Relay Node (Relay)** | Traffic relay services deployed at the public edge, responsible for listening to intranet client persistent connections, acting as the transit bridge between the gateway Agent (OpenResty) and internal traffic. | Node of type `tunnel_relay` running the `openflare-relay` daemon |
| **Penetration Tunnel (Tunnel)** | Logical penetration client instances having a globally unique ID and secure authentication token, used to identify a specific intranet environment. | Globally unique ID generated by Server `tunnel_id` (format: `tun-<32hex>`) |
| **Tunnel Client (Client)** | A lightweight controller running in the intranet environment, automatically managing the underlying frpc tunnel subprocesses according to the configuration dispatched by the Server. | The `openflared` container or independent binary process deployed in the intranet |
| **Tunnel Upstream (Tunnel Upstream)** | A special upstream type in the website configuration. When this type is selected, the gateway forwards public traffic to the Vhost port of the local relay node, eventually reaching the intranet origin. | Upstream of type `tunnel` configured in the website details |
---
## Recommended Operation Sequence
To publish an intranet service to the public internet, we recommend doing so in the following order:
1. Register and deploy at least one public **Relay Node (Relay)** and keep it online.
2. Create a **Penetration Tunnel (Tunnel)** in the management console and copy its dedicated Token.
3. Deploy and start the **Tunnel Client (OpenFlared)** on your intranet server.
4. Confirm that the status of the tunnel in the management console shows as "Online".
5. Add a website configuration, selecting **Intranet Penetration** as the upstream type, binding it to the corresponding tunnel, and entering the intranet port (e.g., `127.0.0.1:8080`).
6. Publish and activate the new version.
7. Access via the public domain to verify that the intranet penetration link is established.
---
## Detailed Configuration Steps
### Step 1: Prepare the Relay Node (Relay)
Intranet traffic is routed through public relay nodes. Before starting, ensure you have a public relay server available.
1. Log into the management console and go to **"Node Management"**.
2. Add a new node, selecting **Relay Node (tunnel_relay)** as the **Node Type**.
3. Save and copy the node-specific `agent_token`.
4. Start the `openflare-relay` process on your public server. You can run it quickly using Docker:
```bash
docker run -d --name openflare-relay --restart unless-stopped \
-p 7000:7000 \
-e OPENFLARE_SERVER_URL=http://<YOUR_SERVER_PUBLIC_IP>:3000 \
-e OPENFLARE_AGENT_TOKEN=<YOUR_COPIED_AGENT_TOKEN> \
-v openflare-relay-data:/var/lib/openflare-relay \
ghcr.io/rain-kl/openflare-relay:latest
```
> [!IMPORTANT]
> Make sure to allow port `7000` (the control port for frpc client connections) in your cloud provider's security group. If your Server and Relay are deployed on the same machine, `OPENFLARE_SERVER_URL` should point to the Server's public or internal IP.
### Step 2: Create a Penetration Tunnel in the Management Console
1. Navigate to the **"Intranet Penetration"** section in the side navigation bar.
2. Click the **"Create Tunnel"** button and enter:
* **Tunnel Name**: Describes the intranet environment, e.g., `home-lab` or `office-dev`.
* **Description**: Optional, describes the purpose of this tunnel.
3. Click save, and the system will automatically generate a globally unique ID and a dedicated `tunnel_token` (e.g., `tun-xxxx...`).
4. Copy the **Client Deployment Command** generated in the popup window, which will be used in the next step.
### Step 3: Deploy the Intranet Client (OpenFlared)
Return to your intranet server and execute the copied deployment command to run the client.
#### Option A: Deploy with Docker (Highly Recommended)
The official `openflared` image embeds the master daemon and `frpc` runtime, working out-of-the-box with no extra dependencies:
```bash
docker run -d --name openflared --restart unless-stopped \
-e OPENFLARE_SERVER_URL=http://<YOUR_SERVER_PUBLIC_IP>:3000 \
-e OPENFLARE_TUNNEL_TOKEN=<YOUR_COPIED_TUNNEL_TOKEN> \
-v openflared-data:/app/data \
ghcr.io/rain-kl/openflared:latest
```
#### Option B: Host Binary Manual Execution
If you cannot use Docker, you can download or compile the `flared` binary:
1. Create a `flared.json` configuration file in the same directory as the executable on your intranet machine:
```json
{
"server_url": "http://<YOUR_SERVER_PUBLIC_IP>:3000",
"tunnel_token": "<YOUR_COPIED_TUNNEL_TOKEN>",
"frpc_path": "/usr/local/bin/frpc",
"data_dir": "./data"
}
```
2. Execute the startup command:
```bash
./flared -config ./flared.json
```
#### Verify Online Status
Once started successfully, the intranet client will send heartbeats through outbound networks to synchronize configurations. At this point:
1. Refresh the **"Intranet Penetration"** list in the management console; the tunnel status indicator should turn green and show **"Online"**.
2. Click tunnel details to view which public Relays the intranet client is currently connected to.
### Step 4: Create a Website and Bind the Tunnel Upstream
Now you can configure public reverse proxy and domain routing for your intranet service.
1. Go to the **"Website Configuration"** page and click **"Create Website"**.
2. Enter the **Domain Name** required to access the service publicly, e.g., `nas.example.com`.
3. Critical Configuration: In the **"Upstream Configuration"** section, switch the **Upstream Type** from "Direct" to **"Intranet Penetration"**.
4. In the dropdown list, select your newly deployed **Intranet Tunnel** (e.g., `home-lab`).
5. Enter the **Intranet Target Address** (the local address and port reachable by the intranet client, e.g., `127.0.0.1:8080`) and select the **Intranet Protocol** (usually `http`).
6. Configure other standard website settings (such as TLS certificates) and click save.
### Step 5: Publish & Activate
To allow the gateway's OpenResty instance to match and route domain traffic correctly, we need to publish a new configuration version.
1. Click **"Preview Config"** in the top right corner of the navigation bar to verify the generated configurations.
2. In the popup window, click **"Publish & Activate"**.
3. Now, the public edge Agent pulls the latest routing, forwarding requests for `nas.example.com` to the loopback virtual host port of `openflare-relay (frps)`.
4. The intranet client `openflared (frpc)` receives the relayed packets, securely hands them over to the local `127.0.0.1:8080` service, and returns responses back through the tunnel.
5. Access `nas.example.com` in your browser to confirm that the intranet service displays successfully!
---
## Advanced Application Scenarios
### 1. Single-Tunnel Multi-Service Multiplexing (Multi-Port Mapping)
You do not need to deploy an `openflared` container for every single internal service.
If you want to map multiple different services in the same intranet environment (e.g., `127.0.0.1:80` for a blog, `127.0.0.1:8080` for an API, and `192.168.1.120:9000` for a local network drive):
1. Keep this single `openflared` client online.
2. Create three independent website configurations in the management console (binding their respective public domains).
3. Set the **Upstream Type** to **the same intranet tunnel** for all three website configurations.
4. Fill in their respective "Intranet Target Addresses" (e.g., `127.0.0.1:80`, `127.0.0.1:8080`, and `192.168.1.120:9000`).
5. Publish and activate the new version to achieve single-tunnel multi-service multiplexing.
### 2. Seamless Integration with Gateway Security Features
Since all public traffic enters the public Agent node first, completing the HTTPS/TLS handshake and WAF filtering before traveling through the secure tunnel:
Your intranet services **naturally benefit from the following advanced features without any code changes**:
* **One-Click HTTPS**: Select or issue SSL certificates directly in the management console, encrypting transmission end-to-end.
* **Global/Custom WAF Protections**: Enables SQL injection blocking, XSS prevention, and regional IP filtering.
* **Human-Machine Challenge (PoW CC)**: Instantly blocks brute-force CC API attacks targeting your intranet services.
---
## Common Troubleshooting
### 1. Tunnel Shows as "Offline" in the Management Console
* **Check the Token**: Check if the `tunnel_token` configured in `flared` logs or environment variables matches the one generated in the management console.
* **Check Outbound Connectivity**: The intranet server must be able to make outbound requests to the Server address. Ensure the control plane firewall is not blocking HTTP requests from the client.
* **Relay Firewall Port Closed**: Check if port `7000` (or your custom bindPort) on the public Relay node has been allowed in the public security groups.
### 2. Accessing the Public Domain Returns 502 Bad Gateway / 504 Gateway Timeout
* **Intranet Service Not Running**: Verify that the service corresponding to the intranet target address is running and listening on the intranet server.
* **Target Address Unreachable**: If the intranet address is set to `127.0.0.1:8080`, ensure the service is running on the exact same host as `openflared`; if set to a LAN IP `192.168.x.x`, test connectivity to that IP inside the `openflared` container.
* **Check Client Application Logs**: View the "Apply Logs" in the management console or inspect local `flared` logs for any `LastError`. When frpc fails to connect to the intranet port, it reports the failure details to the Server.
### 3. Multiple Relays Network Instability or Retry Failures
* When the control plane associates multiple Relay nodes, `openflared` spawns independent frpc daemon processes for each Relay and pulls topology states periodically at `sync_interval` (default 30s) configured in `flared.json`.
* If a Relay drops frequently due to network jitter, the system triggers the backoff retry mechanism automatically. You can see `frpc process missing, starting` logs on the host, which is a normal process self-healing action and will recover within 5-10 seconds after network recovery.
-85
View File
@@ -1,85 +0,0 @@
# Upgrade and Maintenance
You will learn: How to upgrade the Server and Agent, how to clean up observability data, and which verification commands to execute before and after maintenance.
Before upgrading, it is recommended to confirm the current activated version, the latest Agent application result, and the database backup policy. Do not upgrade in production environments while configuration publishing, large-scale Agent reconnection, or database migrations are in progress.
## Server Upgrade
Root users can check and upgrade the Server stable version from the top bar of the management console. Upgrades can also be confirmed and executed by uploading the Server binary.
To try a preview version, you can manually check the corresponding release. It is recommended to prioritize the stable version in production environments.
After upgrading, confirm:
```bash
docker compose ps
docker compose logs -n 100 openflare
```
If it is a source deployment, confirm that there are no database migration or startup errors in the logs after restarting the Server.
## Agent Upgrade
Node Agents follow stable versions by default for automatic updates. Preview upgrades must be triggered manually.
The installation script can be executed repeatedly to reinstall or upgrade the Agent:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--agent-token YOUR_AGENT_TOKEN
```
Note: Currently, the installation script will delete the entire installation directory during reinstallation, including the old `agent.json`, local state, cache data, and downloaded binaries. Please confirm that you still have a usable Token on hand before executing.
After upgrading, confirm:
```bash
systemctl status openflare-agent
journalctl -u openflare-agent -n 100 --no-pager
```
## Data Maintenance
The settings page of the management console can maintain the observability data automatic cleanup policy:
| Configuration Item | Description |
| --- | --- |
| `DatabaseAutoCleanupEnabled` | Whether to enable daily automatic cleanup |
| `DatabaseAutoCleanupRetentionDays` | Automatic cleanup retention days, at least 1 day |
Once enabled, the Server will clean up access logs, metric snapshots, and request reports at 3 AM every day.
## Common Verification Commands
Server:
```bash
cd openflare_server
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Agent:
```bash
cd openflare_agent
GOCACHE=/tmp/openflare-go-cache go test ./...
```
Frontend:
```bash
cd openflare_server/web
pnpm lint
pnpm typecheck
pnpm test
pnpm build
```
Docs:
```bash
cd docs
pnpm build
```
+91 -81
View File
@@ -1,146 +1,156 @@
# Usage
# Basic Usage
You will learn what sites, origins, certificates, versions, nodes, and observability mean in OpenFlare, and which order to follow for daily operations.
You will learn: What website configurations, origins, certificates, versions, nodes, and observability are in OpenFlare, and the recommended sequence of operations during daily usage.
OpenFlare does not patch OpenResty configuration files online. You edit control-plane data in the UI; Agents pull and apply a full configuration only after you publish and activate a new version.
OpenFlare does not directly modify Nginx/OpenResty configurations on nodes online. What you modify in the management console is control plane data; only after publishing and activating a new version will the Agent pull the complete configuration and apply it to the nodes.
## Core Concepts
| Concept | Description |
| --- | --- |
| Site configuration | The reverse proxy aggregation object. One site can bind one or more domains. |
| Primary domain | The first item in the `domains` list. |
| Origin | The upstream service address, such as `http://10.0.0.10:8080`. |
| Configuration version | A full OpenResty configuration snapshot generated by a release. Historical versions are immutable. |
| Active version | The globally effective version. By default, all nodes consume the same active version. |
| Agent | The node-side process that registers, heartbeats, syncs, validates, reloads, and rolls back on failure. |
| Website Config | The aggregate object for reverse proxy rules. One website configuration can bind one or more domains. |
| Primary Domain | The first domain in the `domains` list, used as the main display domain for the website. |
| Origin | The upstream address accessed by the reverse proxy, e.g., `http://10.0.0.10:8080`. |
| Config Version | An immutable snapshot of the complete OpenResty configuration generated upon publishing. |
| Active Version | The globally effective configuration version. All nodes consume the same active version by default. |
| Agent | The node-side process responsible for registration, heartbeats, sync, validation, reloads, and rollbacks on failure. |
## Recommended Workflow
## Recommended Operation Sequence
For a normal reverse proxy change:
When publishing a reverse proxy configuration in daily operations, the following sequence is recommended:
1. Confirm that at least one Agent node is online.
2. Create or select an origin.
3. Create a site configuration with domains, upstreams, and site-level settings.
4. If HTTPS is needed, upload or select certificates and bind them per domain.
5. Preview the rendered configuration or review the diff.
6. Publish and activate a new version.
7. Check node details and apply logs.
2. Add or select an origin address.
3. Create a website configuration, entering the domain, origin, and site-level configurations.
4. If HTTPS is required, upload or select a certificate and bind it by domain.
5. Preview the configuration or review the change summary.
6. Publish and activate the new version.
7. Verify the application result in the node details and application logs.
## Create a Site
## Create Website Configuration
A site requires at least:
A website configuration requires at least:
| Field | Requirement |
| --- | --- |
| Site name | Business-unique identifier. The primary domain is a common default. |
| Domains | At least one domain. The first domain is the primary domain. Each domain must be globally unique. |
| Origin URL | A valid `http://` or `https://` upstream address. |
| Enabled state | Only enabled sites are included in release rendering. |
| Website Name | Business unique identifier; the primary domain is usually used if left blank |
| Domain | At least one domain, where the first is the primary domain; any domain can belong to only one website globally |
| Origin Address | A valid `http://` or `https://` address |
| Enabled Status | Only enabled website configurations will participate in publishing and rendering |
Example:
| Field | Example |
| --- | --- |
| Site name | `docs` |
| Website Name | `docs` |
| Domain | `docs.example.com` |
| Origin URL | `http://10.0.0.10:8080` |
| Origin Host | `docs.internal.example.com` |
| Origin Address | `http://10.0.0.10:8080` |
| Back-to-source Host | `docs.internal.example.com` |
Upstream rules:
Upstream Address Rules:
* A single upstream may include a base path or query string, such as `https://app.example.com/base?from=openflare`.
* Multiple upstreams are used for load balancing and must be plain `scheme://host[:port]`.
* Multiple upstreams in the same site should use the same protocol.
* A single upstream can carry a base path or query, e.g., `https://app.example.com/base?from=openflare`.
* When multiple upstreams are used for load balancing, each upstream must be a pure `scheme://host[:port]`.
* Multiple upstreams in the same rule must use the same protocol.
## Manage Origins
Origins are a lightweight reusable address directory. When a site references an origin, the site still stores a renderable `origin_url` snapshot so historical versions can be replayed independently.
Origins act as a lightweight directory to reuse common upstream addresses. After a website configuration links with an origin, it still stores a renderable snapshot of the `origin_url`, ensuring that historic configuration versions can be re-rendered and rolled back independently.
Recommended practices:
Recommended Practices:
* Store frequently reused internal service addresses as origins.
* After changing an origin entry, check whether site snapshots need to be updated.
* Use preview or diff before publishing.
* Maintain internal service addresses that are frequently reused as Origins.
* After modifying an origin directory, check if published website configurations need their origin snapshots updated.
* Use preview or diff to verify rendering results before publishing.
## Enable HTTPS
HTTPS is bound per domain, not forced for the whole site.
HTTPS is bound by domain rather than being forced across the entire website.
1. Upload or create a certificate record.
2. Open the site configuration and select a certificate for each domain that needs HTTPS.
3. Domains without a certificate stay HTTP-only and are not automatically added to `443 ssl` server blocks.
4. Publish and activate a new version.
Operation Sequence:
If a site contains multiple domains, the Server groups HTTPS output by certificate while keeping all domains in the same site snapshot.
1. Upload or host certificates in the Certificate Management section.
2. Edit the website configuration and select certificates for domains requiring HTTPS.
3. Domains without a bound certificate will remain HTTP and will not be automatically placed in a `443 ssl` server block.
4. Publish and activate the new version.
## Configure WAF and PoW
If a website contains multiple domains, the Server groups and renders the HTTPS configuration by certificate during publishing while keeping these domains within the same website snapshot.
Security controls are managed from the **WAF** sidebar entry:
## Configure WAF & PoW
* The WAF page manages the global rule group and custom rule groups. The global rule group always applies to every site. Custom rule groups can be applied to selected sites from the rule group drawer or bound from the site detail `WAF` section.
* `PoW` is a tab inside the selected rule group, between `Allow / Block Lists` and `Block Response`. It reuses the existing per-site PoW execution logic and can apply the current PoW policy to every site or the sites bound to the current rule group.
* Site details no longer edit PoW directly. They show the always-on global WAF group and let you bind custom WAF rule groups. PoW rule content and scope should be maintained from the WAF page.
Security protection is centrally accessed via the **WAF** link in the side navigation bar:
After changing WAF or PoW settings, publish and activate a new configuration version so Agents can apply the updated OpenResty runtime.
* The WAF page maintains global and custom rule groups. Global rule groups always apply to all websites; custom rule groups can bind websites directly in the group settings or inside the `WAF` section of the website details.
* Clicking **Manage IP Groups** on the WAF page opens the independent IP Groups section. Manual IP groups store IPs/CIDR blocks directly; automatic IP groups evaluate Expr rules against request logs periodically to update members; subscription IP groups periodically sync from remote text or JSON feeds.
* The Auto IP Group page provides two presets: requests count > 100 and 404 ratio >= 80% from a single IP; or IP-host direct access count > 50 and direct access ratio > 50% from a single IP. You can click **Test Rule** to preview IPs matching the log window before saving, and click **Execute Now** to update the group members instantly after saving. The syntax is detailed in [WAF Auto IP Group Expressions](./waf-ip-group-expr.md).
* In the blacklist/whitelist settings of a WAF rule group, you can add IPs/CIDR blocks directly or reference existing IP groups. The published version snapshot only contains referenced IP group IDs; the Agent synchronizes IP group members via checksum differentials and WebSocket real-time broadcasts.
* `PoW` is a configuration Tab in the rule group, located between `Blacklist/Whitelist` and `Block Interception`. It reuses the site's existing PoW execution logic, allowing current PoW parameters to apply to all websites or only those bound to the current rule group.
* The website details page no longer edits individual PoW rules; it only displays the global WAF rule group and binds custom WAF rule groups. The PoW enablement scopes and rule parameters must be maintained centrally on the WAF pages.
## Release, Activate, and Roll Back
After WAF rule groups, site bindings, or PoW configurations are modified, you must republish and activate the configuration version to let the Agent pull and apply them to OpenResty. IP group member changes do not require a new version publication; online Agents update incrementally via WebSockets, while offline or non-WebSocket Agents synchronize via checksum differentials in the next heartbeat.
Standard flow:
For detailed information on WAF security configurations and evaluation principles, see [WAF Security Protection](./waf-usage.md).
## Publish, Activate & Rollback
Standard Pipeline:
```text
Edit configuration -> Preview / diff -> Release -> Generate full version -> Activate version -> Agent pulls -> Agent applies locally -> Agent reports result
Modify config -> Preview / Diff -> Publish -> Generate complete version -> Activate version -> Agent pulls -> Local application -> Report result
```
During release, the Server reads all enabled site configurations, OpenResty main template, performance options, cache options, and certificate assets. It renders a full configuration and calculates a `checksum`.
During publication, the Server reads all enabled website configurations, the main OpenResty config templates, performance and cache parameters, and certificate assets, rendering the complete configuration and calculating its `checksum`.
Rollback means reactivating an old version. The Agent then applies that version through the normal sync flow.
Rolling back does not modify historic versions; it simply re-activates an older version. Once the Agent detects a change in the active version, it pulls and applies it following the standard sync flow.
## Nodes and Observability
## View Nodes & Observability
Node pages answer three questions:
The Nodes section is designed to answer three questions:
| Question | Where to Check |
| Question | Where to check |
| --- | --- |
| Is the node online? | Node list or node detail |
| Which version is running? | Current version on the node detail page |
| Did the last apply succeed? | Apply logs |
| Is the node online? | Node List or Node Details |
| Which version is currently running? | Current Version in Node Details |
| Did the most recent application succeed? | Application Logs |
Node IPs are filled automatically by Agent registration and subsequent heartbeats by default. When you enter or change an IP in the admin UI, the node editor enables "Lock node IP" by default; Agent reports will not overwrite the IP while the lock is enabled. After unlocking, the next Agent heartbeat or WebSocket status report can update it again.
The node IP is automatically filled by Agent registration and heartbeats by default. If you manually enter or modify the IP in the management console, the node edit page defaults to "Lock Node IP"; when enabled, Agent reports will not override this IP. Disabling the lock restores auto-update logic in the next heartbeat or WebSocket state report.
Access analytics and resource snapshots provide basic observability. OpenFlare only keeps access details for a controlled time window; it is not a general-purpose log platform. Use a dedicated logging system for long-term log search.
Traffic Analytics and Resource Snapshots provide basic observability. OpenFlare only retains access details within a controlled time window, and is not positioned as a general logging platform. If you require long-term log indexing, integrate an independent logging system.
## Common Scenarios
### Add a Reverse Proxy for an Internal Service
1. Confirm the Agent node can reach the origin service.
2. Create a site configuration.
3. Add a domain, such as `app.example.com`.
4. Add an origin, such as `http://10.0.0.20:8080`.
1. Verify that the origin service is reachable from the Agent node.
2. Add a website configuration in the management console.
3. Enter the domain, e.g., `app.example.com`.
4. Enter the origin, e.g., `http://10.0.0.20:8080`.
5. Publish and activate the version.
6. Verify the domain from a browser or with `curl`.
6. Verify the domain on the Agent node or from a browser.
> [!TIP]
> If your origin server is deployed internally without a public IP and is unreachable by the Agent, use the intranet penetration tunnel feature to map your service. For detailed instructions, see [Tunnel & Intranet Penetration](./tunnel-usage.md).
### Enable HTTPS for an Existing Domain
1. Prepare a certificate that covers the domain.
2. Upload or create the certificate record.
3. Bind the certificate to the domain in the site configuration.
4. Publish and activate a new version.
5. Verify with `curl -I https://your-domain`.
1. Prepare a certificate covering the domain.
2. Upload or create a certificate record in Certificate Management.
3. Edit the website configuration and select the certificate for the domain.
4. Publish and activate the version.
5. Verify the certificate chain and status code in a browser or via `curl -I https://your-domain`.
### Roll Back a Failed Release
### Roll Back a Failed Publication
1. Open the configuration versions page.
2. Find the last known good version.
3. Activate that version again.
4. Check apply logs until the Agent reports success.
5. Fix the configuration and publish a new version.
1. Open the Configuration Versions page.
2. Locate the last known good version.
3. Re-activate that version.
4. Check the node application logs to verify that the Agent applied the old version.
5. Fix the configuration issues before publishing a new version.
## Recommended Practices
* Set `SESSION_SECRET` explicitly in production and prefer PostgreSQL.
* Preview or diff changes before release.
* Check node details and apply logs after each release.
* Keep the network path from Agents to the Server stable.
* Do not manually edit OpenFlare-managed OpenResty files on nodes; the next release will overwrite them.
* Explicitly configure `SESSION_SECRET` and prefer PostgreSQL in production.
* Review the preview or diff after modifying a website configuration before publishing.
* Check the node details and application logs after every publication.
* Maintain a stable network path from Agent to Server in multi-node deployments.
* Never manually modify OpenResty configurations managed by OpenFlare on the node; these files will be overwritten in the next publication.
+161
View File
@@ -0,0 +1,161 @@
# WAF Auto IP Group Expressions
Automatic IP groups are used to aggregate metrics from request logs on a per-client-IP basis, using Expr expressions to determine if an IP should be added to the group. Automatic IP groups can be referenced by IP blacklists or whitelists in WAF rule groups; during publication, the Server only writes the referenced IP group ID to `waf_config.json`, while IP group members are synchronized independently by the Agent into the local runtime files.
## Configuration Structure
The configuration of an automatic IP group is a JSON object:
```json
{
"lookback_minutes": 60,
"rules": [
{
"name": "Single IP High-Frequency 404 Scanning",
"expr": "request_count > 100 && status_404_ratio >= 0.8"
}
]
}
```
Field Descriptions:
| Field | Type | Role |
| --- | --- | --- |
| `lookback_minutes` | number | How many minutes of request logs to look back during execution. Defaults to 60 minutes if blank, minimum 5 minutes, maximum 43200 minutes. |
| `rules` | array | List of automatic rules. If any rule matches, the IP is added to the automatic IP group list. |
| `rules[].name` | string | Rule name, used only for UI display and error messages. |
| `rules[].expr` | string | Expr expression, must return a boolean value. |
## Evaluation Mechanics
Automatic rules do not evaluate logs request-by-request, but instead aggregate them by client IP first:
1. The Server reads request logs from the past `lookback_minutes` minutes.
2. Groups them by normalized IP (`remote_addr`).
3. Computes metrics like request count, 404 count, and direct IP host count for each IP.
4. Evaluates `rules[].expr` for each IP.
5. If an IP matches any rule, it is written to the automatic IP group's IP member list.
Whether a request is "accessing via IP directly" is determined by the `Host` field in the request logs. If the Host header is an IPv4 or IPv6 literal (e.g., `203.0.113.10`, `[2001:db8::10]`, `203.0.113.10:443`), it is counted in `ip_host_count`.
## Available Metrics
The following metrics are directly available in Expr expressions:
| Keyword | Type | Role |
| --- | --- | --- |
| `ip` | string | The client IP currently being evaluated. |
| `request_count` | number | Total request count of the IP in the lookback window. |
| `status_404_count` | number | Number of 404 responses returned to the IP in the lookback window. |
| `status_404_ratio` | number | 404 request ratio, calculated as `status_404_count / request_count`. |
| `ip_host_count` | number | Number of requests from the IP using an IP address directly as the Host header. |
| `ip_host_ratio` | number | Ratio of direct IP address accesses, calculated as `ip_host_count / request_count`. |
| `client_error_count` | number | Number of requests returning 4xx status codes. |
| `server_error_count` | number | Number of requests returning 5xx status codes. |
| `last_seen_unix` | number | Unix timestamp (in seconds) of the last request from the IP in the lookback window. |
All ratio fields are decimals between `0` and `1`. An 80% ratio should be written as `0.8`, and 50% as `0.5`.
## Common Expr Syntax
Automatic IP groups use the Expr syntax. The expression must return a boolean value.
Common Operators:
| Operator | Role | Example |
| --- | --- | --- |
| `>`, `>=`, `<`, `<=` | Numeric comparison | `request_count > 100` |
| `==`, `!=` | Equality / Inequality | `ip != "127.0.0.1"` |
| `&&` | Logical AND | `request_count > 100 && status_404_ratio >= 0.8` |
| `||` | Logical OR | `status_404_ratio >= 0.8 || server_error_count > 20` |
| `!` | Logical NOT | `!(ip == "127.0.0.1")` |
| `in` | Value is in list | `ip in ["203.0.113.10", "198.51.100.20"]` |
| `not in` | Value is not in list | `ip not in ["127.0.0.1"]` |
| `()` | Grouping controls operator priority | `(request_count > 100 && status_404_ratio >= 0.8) || server_error_count > 50` |
## Built-in Presets
The management console provides two built-in preset rules that can be added directly and adjusted as needed:
```json
{
"name": "Single IP High-Frequency 404 Scanning",
"expr": "request_count > 100 && status_404_ratio >= 0.8"
}
```
Meaning: A single IP requests more than 100 times in the lookback window, and the 404 status code ratio is at least 80%.
```json
{
"name": "Single IP Direct IP Access Mismatch",
"expr": "ip_host_count > 50 && ip_host_ratio > 0.5"
}
```
Meaning: A single IP accesses the server directly using an IP address as the Host header more than 50 times, and this type of access represents more than 50% of its total requests.
## Examples
High-frequency 404 scanning:
```json
{
"lookback_minutes": 60,
"rules": [
{
"name": "High-Frequency 404 Scanning",
"expr": "request_count > 100 && status_404_ratio >= 0.8"
}
]
}
```
Direct IP access mismatch:
```json
{
"lookback_minutes": 30,
"rules": [
{
"name": "Direct IP Access Mismatch",
"expr": "ip_host_count > 50 && ip_host_ratio > 0.5"
}
]
}
```
Capture both high 4xx and 5xx errors:
```json
{
"lookback_minutes": 120,
"rules": [
{
"name": "Abnormal Error Rates",
"expr": "(client_error_count > 80 && request_count > 100) || server_error_count > 30"
}
]
}
```
Exclude trusted IPs:
```json
{
"lookback_minutes": 60,
"rules": [
{
"name": "404 Scanning Excluding Trusted IPs",
"expr": "ip not in [\"203.0.113.10\", \"198.51.100.20\"] && request_count > 100 && status_404_ratio >= 0.8"
}
]
}
```
## Usage Recommendations
Start with a shorter lookback window and higher thresholds to monitor matches, then adjust thresholds gradually. The IP Groups page in the management console allows you to click **"Test Rule"** before saving to view matching IPs in the current window immediately. Once an automatic IP group runs, it overwrites the list of IPs. If you want to permanently whitelist or blacklist certain IPs, add them to a manual IP group instead, and reference both manual and automatic groups in your WAF rule groups.
Updating automatic IP groups does not require publishing configuration versions. Online Agents receive changes via WebSocket and update the local `waf_ip_groups.json` instantly. If WebSocket is unavailable, the Agent reports its local checksum in heartbeats, and the Server syncs only the mismatched IP groups.
+162
View File
@@ -0,0 +1,162 @@
# WAF Security Protection
You will learn: How the OpenFlare edge Web Application Firewall (WAF) works, its protection dimensions, how to manage and reference the three types of IP groups (Manual, Subscription, and Expr-based Automatic IP groups), configure CC protection challenges (PoW human-machine verification) and regional filtering, and achieve sub-second hot updates of IP group members without Nginx reloads.
---
## Core Concepts
Before configuring security policies, you need to understand the core components of the WAF:
| Concept | Description | Scope & Activation Method |
| --- | --- | --- |
| **WAF Rule Group (Rule Group)** | A logical collection of security rules, including: IP whitelists/blacklists (direct input or IP group references), country/region limits, CC protection (PoW), and custom block responses. | Supports global enablement or binding to single/multiple websites. **Modifying rule group definitions requires publishing and activating a configuration version**. |
| **IP Group (IP Group)** | A list container storing individual IPs or CIDR blocks. Divided into **Manual**, **Subscription**, and **Automatic** types. WAF rule groups reference IP groups by ID. | Belongs to dynamic resources. **IP group member updates support sub-second WebSocket hot-syncing, completely bypassing Nginx process reloads**. |
| **PoW Challenge (CC PoW)** | A human-machine verification challenge based on Proof of Work. By prompting browsers to solve hash collisions of a specified difficulty, it silently blocks malicious brute-force scripts and bots while keeping legitimate user experience smooth. | A configuration Tab in the rule group. **Modifying PoW parameters requires publishing and activating a configuration version**. |
---
## Recommended Configuration Sequence
When configuring security protections for your websites, we recommend doing so in the following order:
1. Navigate to IP Groups, creating the required **Manual IP Groups** (e.g., developer whitelist) or **Automatic IP Groups** (e.g., auto-blocked IPs based on 404 scans).
2. Create or edit a **WAF Rule Group**:
* Bind the IP groups you want to reference or block.
* Configure regional whitelists/blacklists for countries or provinces.
* (Optional) Configure human-machine challenge parameters in the `PoW` Tab.
* Set custom status codes (e.g., 403, 418) and HTML block pages in the `Block Response` Tab.
3. Associate the rule group with the corresponding **Website Configuration**.
4. Publish and activate the configuration version to let the edge node (Agent) apply the WAF rules to filter traffic.
---
## Detailed Step Guide
### Step 1: Manage and Configure IP Groups
IP groups are the foundations of large-scale IP filtering. OpenFlare provides three highly resilient types of IP groups:
#### 1. Manual IP Groups (Manual)
* **Purpose**: Statically maintain a list of verified trusted IPs or long-term blocked IPs/CIDR blocks.
* **Configuration**: Click "Create IP Group" -> select type "Manual" -> enter IPs or CIDRs line-by-line (e.g., `192.168.1.100` or `10.0.0.0/24`).
#### 2. Subscription IP Groups (Subscription)
* **Purpose**: Integrate third-party threat intelligence databases or IP ranges published by cloud providers.
* **Configuration**: Select type "Subscription" -> enter fetch URL (supports line-separated plain text or standard JSON formats). A background cron job on the Server periodically pulls the subscription source and updates the group members automatically.
#### 3. Automatic IP Groups (Automatic)
* **Purpose**: **The most aggressive automated defense channel against scans and brute-force attacks**.
* **Configuration**: Select type "Automatic" -> write Expr log aggregation logic. You can directly select built-in presets:
* **Single IP High-Frequency 404 Scanning**: `request_count > 100 && status_404_ratio >= 0.8` (A single IP requesting over 100 times in the past hour with a 404 response ratio of at least 80%).
* **Single IP Direct IP Access Mismatch**: `ip_host_count > 50 && ip_host_ratio > 0.5` (Bypassing domains to hit the server directly using IP address host headers).
* **Test & Run**: Click **"Test Rule"** before saving to preview IPs matching the current log window. Click **"Execute Now"** after saving to aggregate logs immediately and generate the block list.
> [!TIP]
> For the detailed syntax and available metrics of automatic IP groups, see [WAF Auto IP Group Expressions](./waf-ip-group-expr.md).
---
### Step 2: Create and Configure a WAF Rule Group
1. Navigate to the **"WAF"** section in the side menu, and click **"Create Rule Group"**.
2. Enter the rule group name (e.g., `production-api-shield`), and select if it is a "Global Rule Group".
3. Enter rule group details, and configure the tabs sequentially below:
#### 1. Whitelist / Blacklist Configuration (Allow / Block Lists)
* **Direct IPs**: Enter individual IPs or CIDR blocks line-by-line that need temporary whitelisting or blacklisting directly in the text area.
* **IP Group Reference**: Click "Bind IP Groups", selecting the manual, automatic, or subscription IP groups you configured in Step 1. Whitelists permit traffic instantly, whereas blacklists block it.
#### 2. Regional Restriction (GeoIP)
* **Description**: OpenFlare integrates GeoIP geolocation resolution.
* **Configuration**: Toggle the regional restriction switch, selecting "Allow Only" or "Block".
* * For example, if your service is only intended for domestic users, set the mode to "Allow Only" and check `China` in the country list.
* * Supports refining to specific provinces/regions, enabling you to block malicious traffic originating from targeted geographic zones with one click.
#### 3. Human-Machine Challenge Configuration (PoW CC Protection)
* **Description**: Enable CC protection human-machine challenges. When a request triggers the CC protection threshold, the browser renders a silent challenge page, solving a mathematical challenge (hash collision) within several hundred milliseconds. Upon passing, it sets a Cookie and allows subsequent visits. This is seamless to actual users but blocks brute-force scripts and CC tools that do not support JS execution or mathematical computations.
* **Core Parameters**:
* **Status**: Enable / Disable.
* **Hash Difficulty**: Controls the computation difficulty (recommending `4` or `5`).
* **Cookie Expiration**: How long the verification remains valid after passing (e.g., `3600` seconds).
* **Custom Challenge HTML**: Customize the Loading page style of the challenge to match your business design.
#### 4. Block Response (Block Response)
* **Description**: Define the behavior of the WAF when blocking malicious requests.
* **Configuration**:
* **Block Status Code**: Customize the HTTP status code returned, e.g., the standard `403` or a fun `418 (I'm a teapot)`.
* **Block Response Body**: Input custom HTML content shown to blocked attackers (e.g., "WAF Interception: Your request has been logged").
---
### Step 3: Associate the Rule Group with Websites
Once configured, the rule group does not automatically take effect; you need to bind it to specific website configurations.
* **Option A (Recommended)**: In the **"Bind Websites"** Tab of the rule group details, select the websites you wish to apply this rule group to and save.
* **Option B**: Return to **"Website Configuration"**, edit a specific website, and check and bind the rule group in the "Security Protection" section.
> [!NOTE]
> If a rule group is marked as **"Global Rule Group (is_global)"**, it applies to **all websites** hosted on the gateway automatically, requiring no manual binding.
---
### Step 4: Publish & Activate Configurations
1. If you modify **rule group definitions**, **GeoIP scopes**, **PoW CC difficulties**, or **website-to-rule-group bindings**:
* Click **"Preview Config"** -> **"Publish & Activate"** in the top right corner.
* Once the Agent pulls and validates the new version, it rewrites local core OpenResty config files (`waf_config.json`, etc.) and gracefully reloads the processes to apply the policies.
2. If you only update **IP group members** (e.g., adding/deleting an IP in a manual IP group, or an automatic IP group aggregates a new set of blocked IPs periodically):
* **No publication or activation is required!**
* The Server calculates the new MD5 Checksum of the IP group immediately after updating the database.
* The control plane **broadcasts the modified IP group members in real-time to all online Agents via WebSocket**. The Agent overwrites the runtime local disk file `waf_ip_groups.json` incrementally.
* The OpenResty Lua engine calculates the file hash in microseconds when processing new requests. If it detects a Checksum change, it reloads it into the memory dictionary (`ngx.shared`) in real-time. **This entire process requires absolutely no Nginx service reloads, having zero impact on online high-concurrency operations**.
* Even if the WebSocket connection drops, the Agent reports its local Checksum in every heartbeat cycle, and the Server syncs the differential updates to guarantee synchronization.
---
## WAF Evaluation Flow (Filtering Funnel)
When an external request reaches the OpenResty data plane, the WAF runtime evaluates it in the `access` phase according to the funnel decision chain below. Once a match is made, evaluation terminates:
```text
Request enters access phase
│
v
Get all active rule groups bound to this site (Global + Bound Custom groups)
│
v
1. Matches IP whitelist / Whitelist IP group? ──────(Yes)─────► [ Allow (ALLOW) ]
│ (No)
v
2. Matches country / province whitelist? ────────(Yes)─────► [ Allow (ALLOW) ]
│ (No)
v
3. Matches IP blacklist / Blacklist IP group? ──────(Yes)─────► [ Block (BLOCK) ] ──► Return status & HTML block page
│ (No)
v
4. Matches country / province blacklist? ────────(Yes)─────► [ Block (BLOCK) ] ──► Return status & HTML block page
│ (No)
v
5. Is PoW CC protection enabled for this site?
├───(Yes)───► [ Validate PoW Cookie ] ──(Passed)──► [ Allow (ALLOW) ]
│ │
│ (Not Passed)
│ v
│ [ Render PoW Challenge ] ──(Solved)──► Set Cookie & Allow
v
6. No rules triggered, legitimate traffic ─────────────────────► [ Allow (ALLOW) ]
```
---
## Best Practices & Tuning Recommendations
* **Whitelist Precedence & Protection**: Before deploying strict blacklists or regional blocks, we strongly recommend creating a "Trusted IP Group" containing your team's office egress IPs, local development IPs, and third-party callback server IPs (e.g., WeChat or Alipay payment callback addresses), and prioritizing it in the rule group's **whitelist**. This effectively prevents accidental blockages.
* **Reasonably Fine-tune PoW Difficulty**: Human-machine CC challenge hash difficulty (`challenge_difficulty`) is a double-edged sword:
* Difficulty `3`: Computes almost instantly, providing low protection.
* Difficulty `4`: Normal phones/low-end browsers solve it in 100-300ms, providing good protection.
* Difficulty `5`: Requires 500ms-2s, providing strong protection but low-end client browsers might perceive slight loading delays.
* Difficulty `6` and above: Computes exponentially slower, easily freezing client browser CPUs. **We strongly recommend choosing `4` or `5` in production**.
* **Utilize "Test Rule"**: For automatic IP groups, always click **"Test Rule"** before saving. By inspecting the list of matching IPs in the current window, verify if your Expr expressions thresholds (such as request counts, 404 ratios, etc.) are too broad or too strict, preventing accidental blockages of legitimate users.
* **Isolate Static & Dynamic Blacklists**: Never enter static malicious IPs that require permanent blocks directly into automatic IP groups (since the aggregated list will be overwritten in the next cron cycle). You should add permanent malicious IPs into a dedicated "Manual Blacklist IP Group" and reference both the manual and automatic groups in your rule groups.
+18 -15
View File
@@ -3,30 +3,33 @@ layout: home
hero:
name: OpenFlare
text: Self-hosted OpenResty control plane
tagline: Manage reverse proxy rules, configuration releases, node sync, TLS certificates, and basic observability.
text: Open-source CDN Orchestration & Edge Security Platform
tagline: Supports reverse proxy, centralized configuration synchronization, secure intranet penetration (Tunnels), dynamic WAF protection, and anti-CC challenges.
actions:
- theme: brand
text: Quick Start
link: /en/guide/quick-start
- theme: alt
text: Design Boundary
text: Design Boundaries
link: /en/design/
- theme: alt
text: GitHub
link: https://github.com/Rain-kl/OpenFlare
features:
- icon: 🧭
title: Unified Control Plane
details: Manage sites, domains, origins, certificates, nodes, and release state in one console.
- icon: 🚀
title: Immutable Releases
details: Each publish creates a full OpenResty configuration snapshot that can be previewed, activated, and rolled back.
- icon: 🔁
title: Agent Automation
details: Nodes pull, validate, reload, and roll back to the last runnable configuration on failure.
- icon: 📊
title: Basic Observability
details: Includes request rollups, access analytics, resource snapshots, health events, and node details.
- icon: 🛰️
title: Centralized Config Sync
details: Sync configurations across all nodes in real time via WebSockets and heartbeats with sub-second hot reload. Instantly retrieve alerts and statuses.
- icon: 🌐
title: Distributed CDN Orchestration
details: Orchestrate scattered and independent OpenResty nodes into a highly collaborative CDN fleet with website-level multi-domain aggregation and load balancing.
- icon: 🚇
title: Secure Intranet Penetration (Tunnels)
details: An open-source alternative to Cloudflare Tunnels. Expose local intranet services securely to the public network without a public IP or open inbound ports.
- icon: 🛡️
title: Edge WAF Protection
details: Dynamic WAF rules with differential syncing of IP groups to Lua shared memory without Nginx reloads, plus country-level regional access control.
- icon: 🧩
title: Anti-CC & Bot Defense (PoW)
details: Built-in high-performance client-side cryptographic Proof of Work challenges (similar to Turnstile) to intercept botnets and scrapers at the edge.
---
+120 -13
View File
@@ -1,12 +1,12 @@
# API Conventions
You will learn: The response structure, path conventions, authentication methods, and Swagger entry point for OpenFlare management and Agent APIs.
You will learn: The response structure, path conventions, authentication methods, and Swagger entrance for the OpenFlare Admin API and Agent API.
Both the OpenFlare management APIs and Agent APIs use JSON.
Both the OpenFlare Admin API and Agent API communicate using JSON.
## Response Structure
Both success and failure should return a clear `message`:
Both successful and failed API responses must return a clear `message`:
```json
{
@@ -18,31 +18,138 @@ Both success and failure should return a clear `message`:
## Path Conventions
| Type | Convention |
| Category | Convention |
| --- | --- |
| Management API | Authenticated by management console Session |
| Agent API | Fixed under `/api/agent/*` |
| Read-only API | Use `GET` |
| Mutation-type API | Use `POST` |
| Admin API | Authenticated via the Admin Session |
| Agent API | Located strictly under `/api/agent/*` |
| Relay API | Located strictly under `/api/relay/*`, authenticated via `X-Agent-Token` (reusing the Agent's token) |
| OpenFlared API | Located strictly under `/api/flared/*`, authenticated via `X-Tunnel-Token` (dedicated tunnel_token) |
| Read-only APIs | Use the `GET` method |
| Mutating APIs | Use the `POST` method |
## WAF IP Group APIs
The Admin WAF IP Group APIs require Admin Session authentication:
| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/api/waf/ip-groups` | Query IP groups list |
| `GET` | `/api/waf/ip-groups/:id` | Query a single IP group |
| `POST` | `/api/waf/ip-groups` | Create a new IP group |
| `POST` | `/api/waf/ip-groups/test` | Test automatic IP group Expr rules; returns matching IPs in the lookback window without persisting the config |
| `POST` | `/api/waf/ip-groups/:id/update` | Update an existing IP group |
| `POST` | `/api/waf/ip-groups/:id/delete` | Delete an IP group; denied if currently referenced by any rule group |
| `POST` | `/api/waf/ip-groups/:id/sync` | Manually sync subscription IP groups or execute automatic IP group aggregation |
The IP group `type` supports `manual`, `automatic`, and `subscription`. The `auto_config` parameter for automatic IP groups is a JSON object:
```json
{
"lookback_minutes": 60,
"rules": [
{
"name": "Single IP High-Frequency 404 Scanning",
"expr": "request_count > 100 && status_404_ratio >= 0.8"
},
{
"name": "Single IP Direct IP Access Mismatch",
"expr": "ip_host_count > 50 && ip_host_ratio > 0.5"
}
]
}
```
Automatic rules evaluate Expr boolean expressions against metrics aggregated on a per-client-IP basis. The available metrics include `ip`, `request_count`, `status_404_count`, `status_404_ratio`, `ip_host_count`, `ip_host_ratio`, `client_error_count`, `server_error_count`, and `last_seen_unix`. The full syntax is detailed in [WAF Auto IP Group Expressions](../guide/waf-ip-group-expr.md).
Subscription formats support `text` and `json`: plain text parsing resolves one IP or CIDR per line, ignoring empty lines and comments starting with `#`; JSON parsing decodes arrays, reading the root array by default.
## Authentication
The management console continues to reuse the existing login, role, and Session system.
The Admin panel continues to reuse the existing login, role, and Session validation.
Official Agent requests uniformly use the node-exclusive `agent_token`; the first access can use the global `discovery_token`. The Agent request header is fixed as:
Agent requests must carry the node-specific `agent_token` (except for first-time registration, which can use the global `discovery_token`). The header is formatted as:
```http
X-Agent-Token: <token>
```
Full Tokens must not be printed in the logs.
### Agent WAF IP Group Synchronization
The Agent heartbeat payload can carry local WAF IP group checksums:
```json
{
"waf_ip_group_checksums": {
"1": "sha256..."
}
}
```
The Server evaluates the checksums against active configurations, returning mismatched IP groups in the heartbeat response:
```json
{
"waf_ip_groups": [
{
"id": 1,
"name": "Auto Blacklist",
"type": "automatic",
"enabled": true,
"ip_list": ["203.0.113.10"],
"checksum": "sha256..."
}
]
}
```
Alternatively, the Agent can proactively request differential updates upon applying a new configuration version:
| Method | Path | Description |
| --- | --- | --- |
| `POST` | `/api/agent/waf/ip-groups/sync` | Returns mismatched WAF IP groups based on Agent-supplied `ids` and `checksums` |
When an IP group is updated on the Server, connected Agents receive a WebSocket push containing `type = "waf_ip_groups"` with the changed IP groups array as payload. The Agent updates only the changed groups incrementally.
## OpenFlared API
The OpenFlared client communicates with the Server via a dedicated `tunnel_token`, completely decoupled from the Agent authentication system. All endpoints require `X-Tunnel-Token` authentication; requests are denied with `403` if the token is invalid.
| Method | Path | Description |
| --- | --- | --- |
| `POST` | `/api/flared/heartbeat` | Client heartbeat, updates online status and retrieves active tunnel config version summaries |
| `GET` | `/api/flared/config/active` | Pulls the complete tunnel routing configuration (relay list + frpc proxy definitions) |
| `POST` | `/api/flared/apply-log` | Reports configuration application results (success / warning / failed) |
| `GET` | `/api/flared/ws` | Upgrades to a WebSocket connection for real-time `active_config` pushes |
Heartbeat request example:
```http
POST /api/flared/heartbeat
X-Tunnel-Token: <tunnel_token>
Content-Type: application/json
{
"client_version": "v0.2.0",
"frp_version": "0.61.0",
"tunnel_status": "running",
"connected_relays": [
{ "relay_node_id": "node-relay-1", "status": "healthy", "proxy_count": 3 }
],
"current_version": "v1",
"current_checksum": "sha256..."
}
```
The heartbeat response returns the `active_config` summary and `tunnel_settings` (containing runtime settings like heartbeat intervals and WebSocket upgrade switches). When a new configuration version is published, the Server broadcasts a message `type = "active_config"` with the version summary as payload to all connected Clients over WebSockets, prompting them to fetch and apply the config immediately.
Full tokens must never be logged.
## Swagger
Accessible after logging into the management console:
Once logged into the management console, the Swagger page is accessible at:
```text
/swagger/index.html
```
The Swagger files are located in `openflare_server/docs`, generated by `swag init`.
The Swagger definition file is stored in `openflare_server/docs`, generated by `swag init`.
+39 -7
View File
@@ -1,6 +1,6 @@
# Commands and Scripts
# CLI Commands
You will learn: Common commands for starting, building, testing, installing, and uninstalling the OpenFlare Server, management console frontend, Agent, Swagger, and documentation site.
You will learn: Common commands for starting, building, testing, installing, and uninstalling the OpenFlare Server, Admin Frontend, Agent, Swagger, and Documentation site.
## Server
@@ -14,13 +14,13 @@ export LOG_LEVEL='info'
go run .
```
Specify listening port and log directory:
Specify listening port and logging directory:
```bash
go run . --port 3000 --log-dir ./logs
```
Test:
Run tests:
```bash
cd openflare_server
@@ -37,14 +37,14 @@ pnpm install
pnpm dev
```
Build static artifacts:
Build static assets:
```bash
cd openflare_server/web
pnpm build
```
Checks:
Linting and testing checks:
```bash
cd openflare_server/web
@@ -69,13 +69,45 @@ cd openflare_agent
go build -o openflare-agent ./cmd/agent
```
Test:
Run tests:
```bash
cd openflare_agent
GOCACHE=/tmp/openflare-go-cache go test ./...
```
## Relay (Server-side)
Run from source:
```bash
cd openflare_relay
go run ./cmd -config /path/to/relay.json
```
Compile:
```bash
cd openflare_relay
go build -o openflare-relay ./cmd
```
## OpenFlared (Client-side)
Run from source:
```bash
cd openflared
go run ./cmd -config /path/to/flared.json
```
Compile:
```bash
cd openflared
go build -o openflared ./cmd
```
## Install Agent
```bash
+236 -125
View File
@@ -1,98 +1,114 @@
# Configuration Reference
# Configuration Options
You will learn: What configuration sources are supported by OpenFlare Server, frontend builds, and Agents, what the default values of configuration items are, and how common deployment combinations should be configured.
You will learn: What configuration sources are supported by OpenFlare Server, frontend builds, and Agents; what the default configuration values are; and how to configure common deployment combinations.
This document summarizes the Server and Agent configuration items supported by OpenFlare `1.0.0`, retaining only the startup, deployment, and runtime parameters that remain valid.
This document aggregates the currently supported configuration options for OpenFlare Server and Agent in version `1.0.0`, keeping only running parameters that are currently active.
## Configuration Sources
The Server supports three types of configuration sources:
1. Command-line parameters.
1. CLI arguments.
2. Environment variables.
3. Runtime configurations in the database `Option` table.
3. Runtime configurations in the database `options` table.
The Agent supports:
1. `-config` command-line parameter.
2. `agent.json` configuration file.
3. A few log-related environment variables.
1. The `-config` CLI argument.
2. The `agent.json` configuration file.
3. A small set of environment variables for overriding logs and settings.
The Relay (Server-side) supports:
1. The `-config` CLI argument.
2. The `relay.json` configuration file.
3. Persistent environment variables for overriding runtime flags.
The Client (Intranet Client) supports:
1. The `-config` CLI argument.
2. The `flared.json` configuration file.
3. Startup overrides and logging environment variables.
## Configuration File Locations
| Component | Default Location | Description |
| --- | --- | --- |
| Server SQLite | `openflare.db` | Can be modified via `SQLITE_PATH` |
| Agent Configuration File | `./agent.json` | Can be specified via `-config` |
| One-click Install Agent Config | `/opt/openflare-agent/agent.json` | Default generated by the installation script |
| Agent Data Directory | `data` under the config directory | Can be modified via `data_dir` |
| Server SQLite | `openflare.db` | Can be customized via `SQLITE_PATH` |
| Agent Config | `./agent.json` | Can be specified via `-config` |
| One-Click Agent | `/opt/openflare-agent/agent.json` | Generated by the installation script by default |
| Agent Data Dir | `data` in the config folder | Can be customized via `data_dir` |
| Relay Config | `./relay.json` | Can be specified via `-config` |
| One-Click Relay | `/opt/openflare-relay/relay.json` | Generated by the installation script by default |
| Client Config | `./flared.json` | Can be specified via `-config` |
| One-Click Client | `/opt/openflared/flared.json` | Generated by the installation script by default |
## Server CLI Flags
## Server CLI Arguments
```bash
cd openflare_server
go run . --port 3000 --log-dir ./logs
```
| Flag | Purpose | Default |
| Argument | Description | Default Value |
| --- | --- | --- |
| `--port` | Specify the port the Server listens on | `3000` |
| `--log-dir` | Specify the log directory | empty |
| `--version` | Print the current version and exit | `false` |
| `--help` | Print help information and exit | `false` |
| `--port` | Port the Server listens on | `3000` |
| `--log-dir` | Directory to output logs | Empty (stdout) |
| `--version` | Outputs current version and exits | `false` |
| `--help` | Outputs help information and exits | `false` |
## Server Environment Variables
| Variable | Purpose | Default |
| Environment Variable | Description | Default Value |
| --- | --- | --- |
| `PORT` | Server listen port | `3000` |
| `GIN_MODE` | Gin execution mode | `release` unless `debug` |
| `LOG_LEVEL` | Log level | `info` |
| `SESSION_SECRET` | Session signing secret | randomly generated on startup |
| `PORT` | Port the Server listens on | `3000` |
| `GIN_MODE` | Gin framework running mode | Defaults to release unless `debug` |
| `LOG_LEVEL` | Logging level | `info` |
| `SESSION_SECRET` | Session signing key | Randomly generated on startup |
| `SQLITE_PATH` | SQLite database file path | `openflare.db` |
| `DSN` | PostgreSQL DSN, preferred over SQLite when set | empty |
| `SQL_DSN` | Legacy PostgreSQL DSN, lower priority than `DSN` | empty |
| `REDIS_CONN_STRING` | Redis connection string | empty |
| `AGENT_TOKEN` | Legacy global Agent token | empty |
| `DSN` | PostgreSQL DSN (takes precedence over SQLite) | Empty |
| `SQL_DSN` | Legacy PostgreSQL DSN (lower priority than `DSN`) | Empty |
| `REDIS_CONN_STRING` | Redis connection string | Empty |
| `AGENT_TOKEN` | Legacy global Agent Token | Empty |
Description:
Notes:
* When both `DSN` and `SQL_DSN` exist, `DSN` takes precedence.
* When `DSN`/`SQL_DSN` and `SQLITE_PATH` exist simultaneously, PostgreSQL takes precedence.
* When the target PostgreSQL database is empty and a local SQLite file exists at `SQLITE_PATH`, the Server automatically migrates SQLite data at startup and prints table-by-table migration progress in the logs.
* If both `DSN` and `SQL_DSN` exist, `DSN` is prioritized.
* If either `DSN` or `SQL_DSN` coexist with `SQLITE_PATH`, PostgreSQL is prioritized.
* If the target PostgreSQL database is empty and a local SQLite file exists at `SQLITE_PATH`, the Server automatically migrates SQLite data table-by-table on startup.
* `SESSION_SECRET` must be explicitly configured in production.
* When `REDIS_CONN_STRING` is not configured, related capabilities fall back to in-process implementations.
* If `REDIS_CONN_STRING` is unconfigured, co-located features fall back to in-memory implementations.
## Runtime Options
The following options are maintained on the settings page of the management console and can be hot-updated:
The following options are maintained in the admin settings page and support hot reloading:
| Option | Purpose | Default |
| Parameter | Description | Default Value |
| --- | --- | --- |
| `AgentHeartbeatInterval` | Agent heartbeat interval (milliseconds) | `10000` |
| `AgentWebsocketUpgradeEnabled` | Whether to allow Agents to upgrade to WebSockets after successful HTTP heartbeats | `true` |
| `NodeOfflineThreshold` | Node offline threshold (milliseconds) | `120000` |
| `AgentUpdateRepo` | Agent self-update repository | `Rain-kl/OpenFlare` |
| `GeoIPProvider` | Node/IP region lookup provider | `ipinfo` |
| `DatabaseAutoCleanupEnabled` | Whether to enable daily automatic cleanup of observability data | `false` |
| `DatabaseAutoCleanupRetentionDays` | In-database retention days, at least 1 day | `30` |
| `AgentHeartbeatInterval` | Heartbeat interval for Agents (ms) | `10000` |
| `AgentWebsocketUpgradeEnabled` | Toggles WebSocket upgrades after successful HTTP heartbeat | `true` |
| `NodeOfflineThreshold` | Threshold duration to mark a node offline (ms) | `120000` |
| `AgentUpdateRepo` | GitHub repository for Agent self-updates | `Rain-kl/OpenFlare` |
| `GeoIPProvider` | Geolocation resolution provider | `ipinfo` |
| `DatabaseAutoCleanupEnabled` | Toggles daily automatic cleanup of observability logs | `false` |
| `DatabaseAutoCleanupRetentionDays` | Data retention duration in days, minimum 1 day | `30` |
| `GlobalApiRateLimitNum` / `GlobalApiRateLimitDuration` | Global API rate limit count / window | `300` / `180` |
| `GlobalWebRateLimitNum` / `GlobalWebRateLimitDuration` | Global Web rate limit count / window | `300` / `180` |
| `CriticalRateLimitNum` / `CriticalRateLimitDuration` | Sensitive API rate limit count / window | `100` / `1200` |
Description:
Notes:
* When `DatabaseAutoCleanupEnabled` is enabled, the Server automatically cleans up three types of observability data (`node_access_logs`, `node_metric_snapshots`, `node_request_reports`) at 3:00 AM every day.
* `DatabaseAutoCleanupRetentionDays` is the unified retention count and must be greater than or equal to 1.
* The management console supports leaving the retention days blank during manual cleanup to directly delete all history of the corresponding datasets.
* The GitHub Release pointed to by `AgentUpdateRepo` must provide a matching `.sha256` checksum file for each Agent binary, such as `openflare-agent-linux-amd64.sha256`; the self-update validates this SHA-256 digest before replacing the executable.
* Third-party logins no longer use `GitHubOAuthEnabled`, `GitHubClientId`, or `GitHubClientSecret` as primary configuration entries; these legacy options are only used for migration to the default GitHub authentication source during upgrades.
* Legacy options for WeChat login are retained for compatibility, but the management console no longer provides WeChat login configuration entries.
* Legacy options for Turnstile and backend verification remain, and existing configurations will continue to take effect.
* When `DatabaseAutoCleanupEnabled` is enabled, the Server deletes `node_access_logs`, `node_metric_snapshots`, and `node_request_reports` daily at 3:00 AM.
* `DatabaseAutoCleanupRetentionDays` must be greater than or equal to 1.
* Leaving retention days blank during a manual trigger in the console deletes all historic logs instantly.
* The GitHub Release in `AgentUpdateRepo` must contain a matching `.sha256` checksum file for every Agent binary (e.g., `openflare-agent-linux-amd64.sha256`); the Agent validates this checksum before replacing the local executable.
* Third-party logins no longer use `GitHubOAuthEnabled`, `GitHubClientId`, and `GitHubClientSecret` as main configuration entrypoints; these legacy options are used only for migrating default GitHub credentials during upgrades.
* The legacy WeChat login options are kept for backward compatibility, but the option page no longer edits them.
* Legacy Cloudflare Turnstile options and validation logic are retained and will work normally.
## OpenResty Parameters
OpenResty performance and caching parameters continue to be stored uniformly in the `Option` table. Currently common items include:
OpenResty performance and caching parameters are managed in the `options` table, including:
* `OpenRestyWorkerProcesses`
* `OpenRestyWorkerConnections`
@@ -107,96 +123,159 @@ OpenResty performance and caching parameters continue to be stored uniformly in
* `OpenRestyCachePath`
* `OpenRestyCacheMaxSize`
These parameters must be validated, saved, and participate in version rendering in a structured way.
These parameters must be validated, saved, and rendered structurally.
Constraints:
* The management console no longer exposes `resolver` configuration.
* Upstreams are uniformly rendered as named `upstream` blocks with keepalives enabled.
* A single upstream carrying a base path or query will append the original URI in `proxy_pass`.
* Multiple upstreams still require each upstream to be pure `scheme://host[:port]`, and the protocol must be consistent within the same rule.
* `OpenRestyCacheEnabled` is used to enable the caching infrastructure and global default parameters; the actual caching enablement and hit policies (based on URL, suffix, or path) are decided separately by each individual `proxy_routes`.
* The console no longer exposes `resolver` settings.
* Upstreams are rendered uniformly as named `upstream` blocks with keepalive enabled.
* Single upstreams carrying a base path or query have their URI correctly appended in `proxy_pass`.
* Multi-upstreams must be pure `scheme://host[:port]` using the same protocol within a single rule.
* `OpenRestyCacheEnabled` enables cache infrastructure and global defaults; the actual caching matching policies (by URL, suffix, or path) are configured per `proxy_routes`.
* The default cache key is `$scheme$host$request_uri`.
* The default `keepalive_timeout` is `20` seconds, and the default `proxy_connect_timeout` is `3` seconds.
* The default event model is `epoll`, and `multi_accept` is enabled by default.
* HTTPS listeners use the independent `http2 on;` directive by default to avoid deprecation warnings for `listen ... http2` in newer Nginx/OpenResty versions.
* Default `keepalive_timeout` is `20` seconds; default `proxy_connect_timeout` is `3` seconds.
* The default event model is `epoll` with `multi_accept` enabled.
* HTTPS listeners use the independent `http2 on;` directive to avoid deprecation warnings for `listen ... http2` in newer Nginx/OpenResty versions.
## Frontend Build Variables
## Frontend Build Environment Variables
| Variable | Purpose | Default |
| Environment Variable | Description | Default Value |
| --- | --- | --- |
| `NEXT_PUBLIC_API_BASE_URL` | Frontend API request base path | `/api` |
| `NEXT_PUBLIC_APP_VERSION` | Frontend displayed version number | `dev` |
| `NEXT_DEV_BACKEND_URL` | Dev backend proxy target | `http://127.0.0.1:3000` |
| `NEXT_PUBLIC_API_BASE_URL` | Base path for frontend API calls | `/api` |
| `NEXT_PUBLIC_APP_VERSION` | Application version shown in the UI | `dev` |
| `NEXT_DEV_BACKEND_URL` | Target backend proxied by the local dev server | `http://127.0.0.1:3000` |
## Agent Environment Variables
| Variable | Purpose | Default |
| Environment Variable | Description | Default Value |
| --- | --- | --- |
| `LOG_LEVEL` | Agent log level | `info` |
| `OPENFLARE_SERVER_URL` | Control plane URL, can override `agent.json` | empty |
| `OPENFLARE_AGENT_TOKEN` | Node-exclusive auth token, can override `agent.json` | empty |
| `OPENFLARE_DISCOVERY_TOKEN` | Global token for first registration, can override `agent.json` | empty |
| `OPENFLARE_NODE_NAME` | Node name, can override `agent.json` | empty |
| `OPENFLARE_NODE_IP` | Node IP, can override `agent.json` | empty |
| `OPENFLARE_DATA_DIR` | Agent data directory, can override `agent.json` | empty |
| `OPENFLARE_OPENRESTY_PATH` | OpenResty binary path, can override `agent.json` | empty |
| `OPENFLARE_HEARTBEAT_INTERVAL` | Heartbeat interval, can override `agent.json` | empty |
| `OPENFLARE_REQUEST_TIMEOUT` | Request timeout, can override `agent.json` | empty |
| `OPENFLARE_OPENRESTY_OBSERVABILITY_PORT` | Local observability port, can override `agent.json` | empty |
| `OPENFLARE_MMDB_PATH` | WAF GeoIP mmdb path, can override `agent.json` | empty |
| `OPENFLARE_MMDB_UPDATE_INTERVAL` | WAF GeoIP mmdb update interval, can override `agent.json` | empty |
| `OPENFLARE_MMDB_DOWNLOAD_URL` | WAF GeoIP mmdb download URL, can override `agent.json` | empty |
| `LOG_LEVEL` | Logging level for the Agent | `info` |
| `OPENFLARE_SERVER_URL` | Server URL; overrides `agent.json` | Empty |
| `OPENFLARE_AGENT_TOKEN` | Node-specific Token; overrides `agent.json` | Empty |
| `OPENFLARE_DISCOVERY_TOKEN` | Auto-registration Token; overrides `agent.json` | Empty |
| `OPENFLARE_NODE_NAME` | Node name; overrides `agent.json` | Empty |
| `OPENFLARE_NODE_IP` | Node IP; overrides `agent.json` | Empty |
| `OPENFLARE_DATA_DIR` | Agent data directory; overrides `agent.json` | Empty |
| `OPENFLARE_OPENRESTY_PATH` | Path to OpenResty binary; overrides `agent.json` | Empty |
| `OPENFLARE_HEARTBEAT_INTERVAL` | Heartbeat interval; overrides `agent.json` | Empty |
| `OPENFLARE_REQUEST_TIMEOUT` | Request timeout; overrides `agent.json` | Empty |
| `OPENFLARE_OPENRESTY_OBSERVABILITY_PORT` | Local observability port; overrides `agent.json` | Empty |
| `OPENFLARE_MMDB_PATH` | WAF GeoIP mmdb path; overrides `agent.json` | Empty |
| `OPENFLARE_MMDB_UPDATE_INTERVAL` | GeoIP mmdb update interval; overrides `agent.json` | Empty |
| `OPENFLARE_MMDB_DOWNLOAD_URL` | GeoIP mmdb download link; overrides `agent.json` | Empty |
## Agent CLI Flags
## Agent CLI Arguments
| Flag | Purpose | Default |
| Argument | Description | Default Value |
| --- | --- | --- |
| `-config` | Specify the path to the Agent configuration file | `./agent.json` |
| `-config` | Path to the Agent configuration file | `./agent.json` |
## Agent Configuration Fields
## Agent Configurations Fields
| Field | Purpose | Required | Default / Behavior |
| Field | Description | Required | Default Value / Behavior |
| --- | --- | --- | --- |
| `server_url` | Control plane URL | yes | none |
| `agent_token` | Node-exclusive auth token | one of `agent_token`/`discovery_token` | empty |
| `discovery_token` | Global token for first registration | one of `agent_token`/`discovery_token` | empty |
| `node_name` | Node name | no | automatically uses host name |
| `node_ip` | Node IP | no | auto-detected; prioritizes obtaining the real public egress IP via third-party APIs, falling back to local interfaces on failure |
| `openresty_path` | OpenResty binary path | no | `openresty` |
| `openresty_observability_port` | Local observability and OpenResty health-check port | no | `18081` |
| `data_dir` | Agent data directory | no | `data` under the config file directory |
| `main_config_path` | OpenResty main config write path | no | `data_dir/etc/nginx/nginx.conf` |
| `route_config_path` | Route config write path | no | `data_dir/etc/nginx/conf.d/openflare_routes.conf` |
| `access_log_path` | OpenResty access log path | no | `data_dir/var/log/openflare/access.log` |
| `cert_dir` | Certificate write directory | no | `data_dir/etc/nginx/certs` |
| `openresty_cert_dir` | Certificate read directory in OpenResty config | no | same as `cert_dir` |
| `lua_dir` | Lua scripts and static resources write directory | no | `data_dir/etc/nginx/lua` |
| `openresty_lua_dir` | Lua read directory in OpenResty config | no | same as `lua_dir` |
| `runtime_config_dir` | Agent runtime config write directory, e.g., `pow_config.json` | no | `data_dir/etc/openflare` |
| `mmdb_path` | WAF GeoIP mmdb file path | no | `data_dir/etc/openflare/GeoLite2-Country.mmdb` |
| `mmdb_update_interval` | WAF GeoIP mmdb update interval | no | `86400000` milliseconds |
| `mmdb_download_url` | WAF GeoIP mmdb download URL | no | built-in GeoLite2 Country download URL |
| `observability_buffer_path` | Observability buffering file path | no | `data_dir/var/lib/openflare/observability-buffer.json` |
| `observability_replay_minutes` | Minutes to automatically replay recent observability data | no | `15` |
| `state_path` | Agent local state file path | no | `data_dir/var/lib/openflare/agent-state.json` |
| `heartbeat_interval` | Heartbeat interval | no | `10000` milliseconds |
| `request_timeout` | HTTP request timeout | no | `10000` milliseconds |
| `server_url` | Control plane URL | Yes | None |
| `agent_token` | Node-specific access Token | Mutually exclusive with discovery_token | Empty |
| `discovery_token` | Global auto-registration Token | Mutually exclusive with agent_token | Empty |
| `node_name` | Node name | No | Hostname |
| `node_ip` | Node IP | No | Auto-detect, resolves outbound public IP via realip.cc first, falls back to local adapters |
| `openresty_path` | Path to the OpenResty binary | No | `"openresty"` |
| `openresty_observability_port` | Observability port for health checks | No | `18081` |
| `data_dir` | Agent data directory | No | `data` in the config folder |
| `main_config_path` | Write path for Nginx main configuration | No | `data_dir/etc/nginx/nginx.conf` |
| `route_config_path` | Write path for route configurations | No | `data_dir/etc/nginx/conf.d/openflare_routes.conf` |
| `access_log_path` | Write path for OpenResty access logs | No | `data_dir/var/log/openflare/access.log` |
| `cert_dir` | Write directory for SSL certificates | No | `data_dir/etc/nginx/certs` |
| `openresty_cert_dir` | Read directory for certificates in Nginx | No | Same as `cert_dir` |
| `lua_dir` | Write directory for Lua scripts and assets | No | `data_dir/etc/nginx/lua` |
| `openresty_lua_dir` | Read directory for Lua scripts in Nginx | No | Same as `lua_dir` |
| `runtime_config_dir` | Write directory for Agent runtime configs | No | `data_dir/etc/openflare` |
| `mmdb_path` | WAF GeoIP database file path | No | `data_dir/etc/openflare/GeoLite2-Country.mmdb` |
| `mmdb_update_interval` | WAF GeoIP database check interval | No | `86400000` milliseconds |
| `mmdb_download_url` | WAF GeoIP database download URL | No | Built-in GeoLite2 Country URL |
| `observability_buffer_path` | Buffer path for retry metrics logs | No | `data_dir/var/lib/openflare/observability-buffer.json` |
| `observability_replay_minutes` | Lookback window for metric retries | No | `15` |
| `state_path` | Path to store local state JSON file | No | `data_dir/var/lib/openflare/agent-state.json` |
| `heartbeat_interval` | Heartbeat polling interval | No | `10000` milliseconds |
| `request_timeout` | HTTP request timeout duration | No | `10000` milliseconds |
Description:
Notes:
* `agent_token` and `discovery_token` cannot both be empty.
* `heartbeat_interval` and `request_timeout` support integer milliseconds or Go duration strings.
* When the Server runtime option `AgentWebsocketUpgradeEnabled` is enabled, the Agent will attempt to upgrade to a WebSocket after a successful HTTP heartbeat; it automatically falls back to HTTP heartbeats when connection fails or is disconnected.
* When `openresty_path` is not configured, `openresty` is called by default.
* The Agent's periodic health checks request `http://127.0.0.1:<openresty_observability_port>/openflare/stub_status`, no longer judging runtime health via high-frequency `openresty -t`; validation before configuration application, startup recovery, and reloads will still execute `openresty -t -c <main_config_path>`.
* The Agent initializes and periodically updates `mmdb_path` for OpenResty WAF Lua to execute country-level geographical rules; update failures only record warnings, and do not block sync or reloads.
* If `agent.json` does not exist but environment variables such as `OPENFLARE_SERVER_URL` and tokens are sufficient, the Agent can start directly; environment variables take precedence when both exist.
* When the Agent is not configured with `node_ip`, it first queries `https://realip.cc` for the real public egress IP, adapting to Docker/NAT scenarios; it falls back to local interface detection on failure, preferring a public IPv4 address.
* When the Agent automatically detects a private `node_ip`, the Server prioritizes retaining the public address of the Agent's direct connection during registration/heartbeat phases, avoiding misregistering internal interface addresses in NAT or multi-interface scenarios.
* When "Lock node IP" is enabled in the admin UI, the Server keeps the manually configured node IP and Agent registration, HTTP heartbeat, or WebSocket status reports will not overwrite that field; after unlocking, the next report can fill it again.
* If `AgentWebsocketUpgradeEnabled` is enabled on the Server, the Agent upgrades the HTTP heartbeat to WebSocket; it automatically falls back to HTTP heartbeats if it fails or disconnects.
* If `openresty_path` is left blank, the Agent calls `openresty` on the host.
* Periodic health checks query `http://127.0.0.1:<openresty_observability_port>/openflare/stub_status` instead of executing `openresty -t`; validation prior to reloads, starts, or rollbacks still runs `openresty -t -c <main_config_path>`.
* The Agent initializes and periodically updates `mmdb_path` to support GeoIP region checks; failures to update write warnings and do not disrupt configuration synchronizations.
* The Agent boots normally if `agent.json` is missing but environment variables (`OPENFLARE_SERVER_URL` and a Token) are available; environment variables override JSON settings.
* If `node_ip` is left blank, the Agent resolves its outbound IP via `https://realip.cc` first, which is suitable for Docker/NAT networks.
* If the Agent registers a private `node_ip`, the Server prioritizes saving the public TCP connection IP, preventing NAT adapters from registering internal IPs.
* Enabling "Lock Node IP" in the console retains the manual IP; subsequent Agent registration or heartbeats do not overwrite it.
## Common Configuration Combinations
## Relay Environment Variables
| Environment Variable | Description | Default Value |
| --- | --- | --- |
| `LOG_LEVEL` | Logging level for the Relay | `info` |
| `OPENFLARE_SERVER_URL` | Server URL; overrides `relay.json` | Empty |
| `OPENFLARE_AGENT_TOKEN` | Node-specific Token; overrides `relay.json` | Empty |
| `OPENFLARE_DISCOVERY_TOKEN` | Auto-registration Token; overrides `relay.json` | Empty |
| `OPENFLARE_NODE_NAME` | Node name; overrides `relay.json` | Empty |
| `OPENFLARE_NODE_IP` | Node IP; overrides `relay.json` | Empty |
| `OPENFLARE_DATA_DIR` | Relay data directory; overrides `relay.json` | Empty |
| `OPENFLARE_FRPS_PATH` | frps binary path; overrides `relay.json` | Empty |
## Relay CLI Arguments
| Argument | Description | Default Value |
| --- | --- | --- |
| `-config` | Path to the Relay configuration file | `./relay.json` |
## Relay Configuration Fields
| Field | Description | Required | Default Value / Behavior |
| --- | --- | --- | --- |
| `server_url` | Control plane URL | Yes | None |
| `agent_token` | Node-specific access Token | Mutually exclusive with discovery_token | Empty |
| `discovery_token` | Global auto-registration Token | Mutually exclusive with agent_token | Empty |
| `node_name` | Node name | No | Hostname |
| `node_ip` | Relay listening IP for tunnel traffic | No | Auto-detect, prioritizes outbound public IP |
| `frps_path` | Path to the `frps` binary | No | `frps` (system PATH) |
| `data_dir` | Relay runtime data directory | No | `data` in the config folder |
| `state_path` | Path to store local state JSON file | No | `data_dir/relay-state.json` |
| `heartbeat_interval` | Heartbeat polling interval | No | `10000` milliseconds, supports Go duration strings |
| `request_timeout` | HTTP request timeout duration | No | `10000` milliseconds, supports Go duration strings |
## OpenFlared (Client) Environment Variables
| Environment Variable | Description | Default Value |
| --- | --- | --- |
| `LOG_LEVEL` | Logging level for the client | `info` |
| `OPENFLARE_SERVER_URL` | Server URL; overrides `flared.json` | Empty |
| `OPENFLARE_TUNNEL_TOKEN` | Tunnel access Token; overrides `flared.json` | Empty |
| `OPENFLARE_DATA_DIR` | Client data directory; overrides `flared.json` | Empty |
| `OPENFLARE_FRPC_PATH` | frpc binary path; overrides `flared.json` | Empty |
## OpenFlared (Client) CLI Arguments
| Argument | Description | Default Value |
| --- | --- | --- |
| `-config` | Path to the client configuration file | `./flared.json` |
## OpenFlared (Client) Configuration Fields
| Field | Description | Required | Default Value / Behavior |
| --- | --- | --- | --- |
| `server_url` | Control plane URL | Yes | None |
| `tunnel_token` | Tunnel dedicated access Token | Yes | None |
| `frpc_path` | Path to the `frpc` binary | No | `frpc` (system PATH) |
| `data_dir` | Client runtime data directory | No | `data` in the config folder |
| `state_path` | Path to store local state JSON file | No | `data_dir/flared-state.json` |
| `heartbeat_interval` | Heartbeat polling interval | No | `10000` milliseconds, supports Go duration strings |
| `sync_interval` | Configuration sync interval | No | `30000` milliseconds, supports Go duration strings |
| `request_timeout` | HTTP request timeout duration | No | `10000` milliseconds, supports Go duration strings |
## Common Configuration Combos
### Production Server + PostgreSQL
@@ -229,7 +308,7 @@ go run .
}
```
### Agent + Customized OpenResty Path
### Agent + Customized OpenResty Paths
```json
{
@@ -248,12 +327,44 @@ go run .
}
```
## Maintenance Requirements
### Relay (Server-side) Default Configuration
When the following contents change, this document must be updated in sync:
`relay.json`:
* Server command-line parameters.
```json
{
"server_url": "http://your-server:3000",
"agent_token": "replace-with-relay-auth-token",
"frps_path": "frps",
"data_dir": "/opt/openflare-relay/data",
"heartbeat_interval": 10000,
"request_timeout": 10000
}
```
### OpenFlared (Client-side) Default Configuration
`flared.json`:
```json
{
"server_url": "http://your-server:3000",
"tunnel_token": "replace-with-tunnel-token",
"frpc_path": "frpc",
"data_dir": "/opt/openflared/data",
"heartbeat_interval": 10000,
"sync_interval": 30000,
"request_timeout": 10000
}
```
## Maintenance Rules
This document must be updated in sync when any of the following change:
* Server CLI arguments.
* Server environment variables.
* Agent command-line parameters.
* Agent configuration fields.
* Default values, purposes, or examples of any configuration items.
* Agent CLI arguments and configuration parameters.
* Relay CLI arguments and configuration parameters.
* Client CLI arguments and configuration parameters.
* Default values, scopes, or examples of any configuration items.
+8 -7
View File
@@ -1,12 +1,13 @@
# Reference
# Reference Manuals
You will learn: What information belongs to stable reference materials, and where to look up configurations, commands, APIs, and the repository structure.
You will learn: Which information belongs to stable reference manuals, and where to look up configurations, commands, APIs, and repository structures.
This section collects stable information at the runtime, interface, and repository levels, suitable for quick lookups during deployment, joint debugging, and troubleshooting.
This section collects stable information at the runtime, API, and repository layers, suitable for rapid lookup during deployment, integration, and troubleshooting.
| Page | Content |
| --- | --- |
| [Configuration Items](./configuration.md) | Server environment variables, command line parameters, runtime Options, and Agent configuration fields |
| [Commands and Scripts](./cli.md) | Common startup, build, test, install, and uninstall commands |
| [API Conventions](./api.md) | Response structure, authentication, and path conventions of management and Agent APIs |
| [Repository Layout](./repository.md) | Responsibilities of `openflare_server`, `openflare_agent`, `openflare_server/web`, and `docs` |
| [Configuration Options](./configuration.md) | Server environment variables, CLI arguments, runtime Options, and Agent configuration parameters |
| [CLI Commands](./cli.md) | Common CLI commands for starting, building, testing, installing, and uninstalling |
| [API Conventions](./api.md) | Response structures, authentication, and routing paths for Admin and Agent APIs |
| [Repository Structure](../design/repository.md) | Scope of responsibilities and folder layering of the Server, Agent, Relay, and Client |
| [Deployment & Upgrade](../deployment/) | Server and Agent deployment, configuration, and upgrade guides (dedicated section) |
-47
View File
@@ -1,47 +0,0 @@
# Repository Layout
You will learn: What the Server, Agent, frontend, scripts, and documentation directories in the OpenFlare repository are responsible for, and which layer to place your logic when contributing code.
| Path | Responsibility |
| --- | --- |
| `openflare_server` | Gin + GORM + SQLite/PostgreSQL monolithic control plane |
| `openflare_server/web` | Next.js 15 App Router management console frontend, statically exported and hosted by the Go Server |
| `openflare_agent` | Go monolithic Agent, running on the node side |
| `scripts` | Helper scripts such as Agent installation, uninstallation, etc. |
| `docs` | VitePress documentation site, design baseline, development constraints, deployment, and configuration documents |
## Server Layering
| Directory | Responsibility |
| --- | --- |
| `controller/` | Parameter parsing, calling services, returning responses |
| `service/` | Business logic, verification, transaction orchestration, configuration rendering |
| `model/` | Model definition, database version, and migration |
| `router/` | Route registration |
| `middleware/` | Auth, authorization, rate limiting, and other cross-cutting logic |
| `common/` | Configuration, global state, and initialization entry points |
| `utils/` | Pure utility functions and general helpers |
## Agent Modules
| Module | Responsibility |
| --- | --- |
| `config` | Configuration reading and default values |
| `heartbeat` | Heartbeat and version summary judgment |
| `sync` | Configuration pulling and application orchestration |
| `nginx` / `openresty` | OpenResty file writing, verification, reload, startup, and rollback |
| `state` | Local state and observability supplementary reporting buffer |
| `httpclient` | Server communication |
| `protocol` | Agent API protocol types |
| `internal/updater` | Agent self-updating |
## Frontend Layering
| Directory | Responsibility |
| --- | --- |
| `app/` | Routes, layouts, page assembly |
| `features/` | Organize modules by business domains |
| `components/` | Reuse components across features |
| `lib/` | Request client, environment variables, utility functions, constants |
| `store/` | A small amount of cross-page UI state |
| `types/` | Shared type definitions |
+29
View File
@@ -0,0 +1,29 @@
# 引用与致谢
OpenFlare 本质上是一个方案整合项目, 在设计与实现过程中借鉴了众多开源项目的优秀理念、架构设计和技术实现。以下是 OpenFlare 在核心底层引擎、安全防护机制以及前后端系统框架等方面所引用的关键开源项目,以及对这些项目及其社区的感谢。
---
### 1. OpenResty
* **项目定位**:基于 Nginx 与 Lua 的高性能 Web 平台。
* **在 OpenFlare 中的作用**:作为全局数据面(Data Plane)的边缘网关。所有的公网 Web 流量均首先由 OpenResty 接收,在此处进行高并发的 HTTPS 握手、WAF 安全规则比对、防 CC 人机验证,并最终执行反向代理转发。
* **项目链接**:[OpenResty 官网](https://openresty.org/)
### 2. FRP (Fast Reverse Proxy)
* **项目定位**:高性能的反向代理应用,专注于内网穿透。
* **在 OpenFlare 中的作用**:作为内网穿透子系统的底层隧道引擎。中继端管理器 `openflare-relay` 负责守护和调度 `frps` 引擎,而内网客户端 `openflared` 则负责在本地自动生成 TOML 配置并守护多路复用 `frpc` 子进程。
* **项目链接**:[fatedier/frp (GitHub)](https://github.com/fatedier/frp)
---
### 3. Anubis (PoW 方案)
* **项目定位**:基于工作量证明(Proof of Work)的轻量级人机验证防护方案。
* **在 OpenFlare 中的作用**:为网关 WAF 提供了核心的**无感防 CC 人机挑战**能力。
---
### 4. gin-template
* **项目定位**:基于 Go Gin 与前端构建的现代化全栈开发脚手架模板。
* **在 OpenFlare 中的作用**:为 OpenFlare 控制面(Server)提供了规范、统一的前后端系统架构雏形。
---
+14 -9
View File
@@ -10,10 +10,12 @@ OpenFlare 是一套自托管的 OpenResty 控制面。它把反向代理网站
1. [快速开始](./quick-start.md):用 Docker Compose 启动 Server,登录管理端,并接入第一个 Agent。
2. [基础使用](./usage.md):了解网站配置、源站、证书、发布、回滚和观测的常见操作。
3. [WAF 自动 IP 组语法](./waf-ip-group-expr.md):编写自动 IP 组 Expr 规则,了解关键字含义和预设规则。
4. [部署说明](../reference/deployment.md):把 Server 和 Agent 放到更接近生产的环境中运行。
5. [配置项参考](../reference/configuration.md):查 Server 环境变量、运行时 Option 和 Agent 配置字段。
6. [故障排查](./troubleshooting.md):按症状排查登录、数据库、节点同步、OpenResty 应用和前端构建问题。
3. [内网穿透与隧道使用](./tunnel-usage.md):学习部署 Relay 与 Client,实现安全、无公网 IP 反向穿透。
4. [WAF 安全防护使用](./waf-usage.md):掌握 IP 黑白名单、自动 IP 组 Expr 自动聚合、地域限制与 PoW CC 防护。
5. [WAF 自动 IP 组语法](./waf-ip-group-expr.md):编写自动 IP 组 Expr 规则,了解关键字含义和预设规则。
6. [部署说明](../deployment/deployment.md):把 Server 和 Agent 放到更接近生产的环境中运行。
7. [配置项参考](../reference/configuration.md):查 Server 环境变量、运行时 Option 和 Agent 配置字段。
8. [故障排查](./troubleshooting.md):按症状排查登录、数据库、节点同步、OpenResty 应用和前端构建问题。
## 按角色查找
@@ -21,13 +23,16 @@ OpenFlare 是一套自托管的 OpenResty 控制面。它把反向代理网站
| --- | --- |
| 5 分钟内跑起管理端 | [快速开始](./quick-start.md) |
| 发布第一条反向代理配置 | [发布第一份配置](./first-site.md) |
| 配置内网穿透映射 | [内网穿透与隧道使用](./tunnel-usage.md) |
| 配置防 CC 与 IP 组拦截 | [WAF 安全防护使用](./waf-usage.md) |
| 编写自动 IP 组规则 | [WAF 自动 IP 组语法](./waf-ip-group-expr.md) |
| 接入或重装节点 Agent | [接入 Agent](../reference/agent.md) |
| 从源码启动 Server | [启动 Server](../reference/server.md) |
| 接入或重装节点 Agent | [接入 Agent](../deployment/agent.md) |
| 从源码启动 Server | [启动 Server](../deployment/server.md) |
| 配置 GitHub 或 OIDC 登录 | [SSO 登录配置](./sso.md) |
| 升级 Server 或 Agent | [升级与维护](../reference/upgrade.md) |
| 升级 Server 或 Agent | [升级与维护](../deployment/upgrade.md) |
| 参与开发或修复问题 | [本地开发](../design/development.md) 与 [开发约束](../guildline/development-constraints.md) |
| 理解架构和发布模型 | [系统架构](../design/architecture.md) 与 [发布模型](../design/release-model.md) |
| 理解架构和发布模型 | [系统架构](../design/architecture.md) 与 [Agent 与发布模型](../design/agent-design.md) |
| 查看开源引用与致谢 | [引用与致谢](./credits.md) |
## 文档分区
@@ -35,4 +40,4 @@ OpenFlare 是一套自托管的 OpenResty 控制面。它把反向代理网站
`reference/` 收敛稳定事实,例如配置字段、命令、API 响应约定和仓库结构。
`design/` 面向维护者和贡献者,描述产品边界、系统架构、发布模型和工程约束。新增能力或改变边界前,应先更新对应设计文档。
`design/` 面向维护者和贡献者,描述产品边界、系统架构、Agent 与发布模型和工程约束。新增能力或改变边界前,应先更新对应设计文档。
+23 -4
View File
@@ -21,7 +21,8 @@ Agent 统一通过 OpenResty 二进制控制运行时。本地部署需要节点
| 可访问端口 | Server 默认监听 `3000`,Agent 节点需要能访问 Server 地址 |
| 浏览器 | 用于访问管理端 |
[需要确认:项目建议的最低 Docker 与 Docker Compose 版本]
- **Docker**:`20.10.0+`
- **Docker Compose**:`2.0.0+`
## 1. 启动 Server
@@ -53,13 +54,16 @@ services:
ports:
- "3000:3000"
environment:
SESSION_SECRET: replace-with-a-long-random-string
JWT_SECRET: replace-with-a-long-random-string
DSN: postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable
GIN_MODE: release
LOG_LEVEL: info
volumes:
- openflare-data:/data
volumes:
postgres-data:
openflare-data:
```
启动服务:
@@ -100,7 +104,8 @@ Agent 可以用两类凭证接入:
在管理端准备其中一种凭证后,进入下一步。
[需要确认:当前管理端中创建或查看 `discovery_token` 与节点 `agent_token` 的准确菜单路径]
- **`discovery_token`** 获取菜单路径:「系统设置」->「自动注册」
- **`agent_token`** 获取菜单路径:「节点管理」->「新增节点」
## 3. 安装/运行 Agent
@@ -114,7 +119,7 @@ Agent 部署方式推荐使用 Docker 部署(即直接运行内置 OpenResty
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443 \
-p 80:80 -p 443:443/tcp -p 443:443/udp \
-v openflare-agent-data:/data \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
@@ -199,3 +204,17 @@ journalctl -u openflare-agent -n 100 --no-pager
| OpenResty 应用失败 | 查看节点应用记录和 `journalctl -u openflare-agent`,重点检查域名、证书、上游地址和端口占用 |
更多排查路径见 [故障排查](./troubleshooting.md)。
---
## 进阶部署指引
当您完成快速开始并熟悉了 OpenFlare 的基本操作后,可以阅读以下进阶部署文档,将各组件投入到正式生产环境中:
* **Server 生产部署**:阅读 [启动 Server](../deployment/server.md) 了解如何从源码构建前端、配置系统环境变量及使用 Docker Compose 运行。
* **Agent 生产接入**:阅读 [部署 Agent](../deployment/agent.md) 了解基于 systemd 的服务管理、详细本地配置文件字段及故障排查。
* **内网穿透中继端部署**:阅读 [部署 Relay](../deployment/relay.md) 了解如何为穿透隧道配置公网中继节点(frps)。
* **内网穿透客户端部署**:阅读 [部署 OpenFlared](../deployment/openflared.md) 了解如何在内网服务器侧运行穿透守护客户端(frpc)。
* **生产部署拓扑参考**:阅读 [部署说明](../deployment/deployment.md) 了解生产高可用拓扑和整体网络规划。
* **系统升级与日常维护**:阅读 [升级与维护](../deployment/upgrade.md) 了解如何平滑升级 Server 和各代理节点 Agent。
+24 -4
View File
@@ -9,7 +9,7 @@
| 现象 | 先看哪里 |
| --- | --- |
| 管理端打不开 | Server 容器或进程日志、端口监听 |
| 登录异常 | 默认账号、Session Secret、浏览器请求、Server 日志 |
| 登录异常 | 默认账号、OPENFLARE_TOKEN、浏览器请求、Server 日志 |
| 数据无法保存 | 数据库连接、SQLite 文件权限、PostgreSQL 健康状态 |
| Agent 离线 | Agent 日志、Token、Server 地址、网络连通性 |
| 发布后节点未更新 | 激活版本、节点 heartbeat、应用记录 |
@@ -85,10 +85,30 @@ NEXT_DEV_BACKEND_URL=http://127.0.0.1:3000 pnpm dev
1. 确认连接的是预期数据库,避免 `SQLITE_PATH` 或 `DSN` 指向了另一个环境。
2. 查看 Server 日志中使用的是 `sqlite` 还是 `postgres`。
3. 如果部署在多副本或反向代理后,确认 `SESSION_SECRET` 固定且各实例一致。
4. 清理浏览器 Cookie 后重新登录。
3. 在浏览器开发者工具中确认管理端 API 请求携带 `OPENFLARE_TOKEN` 请求头。
4. 清理浏览器本地存储中的旧 `openflare_token` 后重新登录。
[需要确认:当前项目是否提供安全的 root 密码重置命令或流程]
### 应急重置管理员密码
如果忘记了 `root` 账户的密码,可以通过直接更新数据库中的密码哈希值将其重置为 `123456`(登录后请务必立即修改):
#### 1. 若使用 SQLite 数据库
停止 Server 运行,使用 sqlite3 客户端打开数据库文件:
```bash
sqlite3 /path/to/openflare.db
```
执行以下 SQL 语句:
```sql
UPDATE users SET password_hash = '$2a$10$wN9aE3zTz83rO7R1uKlhuehJtA3c604pX4Z12B/9.5c0X337t1L4m' WHERE username = 'root';
```
输入 `.exit` 退出并重新启动 Server。
#### 2. 若使用 PostgreSQL 数据库
通过您的数据库连接工具(如 psql、pgAdmin 或 DBeaver)连接到 PostgreSQL 实例,选择对应的 `openflare` 数据库,执行以下 SQL 语句:
```sql
UPDATE users SET password_hash = '$2a$10$wN9aE3zTz83rO7R1uKlhuehJtA3c604pX4Z12B/9.5c0X337t1L4m' WHERE username = 'root';
```
执行成功后即可使用默认密码 `123456` 重新登录管理后台。
## Agent 无法注册或一直离线
+174
View File
@@ -0,0 +1,174 @@
# 内网穿透与隧道使用
你会学到:OpenFlare 内网穿透隧道的设计原理、核心概念(中继节点与隧道客户端),以及如何从零开始将内网开发环境或私有云服务一步步安全、稳定地发布到公网域名上。
在许多实际开发和运维场景中,我们的源站服务部署在局域网、本地开发机或防范严密的私有 VPC 内部,没有公网 IP,亦无法在边界防火墙或路由器上配置端口映射。
OpenFlare 提供了**基于反向中继穿透隧道**的整体解决方案。你只需在内网环境发起向公网中继节点的出向安全连接,无需配置任何入方向端口,即可将公网的 Web 访问流量平滑引入内网源站,同时享有网关提供的 TLS 证书自动托管与 WAF 安全防护。
---
## 核心概念
在使用内网穿透功能前,你需要熟悉以下组件与核心概念:
| 概念 | 说明 | 对应组件/操作 |
| --- | --- | --- |
| **中继节点 (Relay)** | 部署在公网边缘的流量中继服务,负责监听内网客户端的长连接,并作为网关 Agent (OpenResty) 与内网流量的中转桥梁。 | 运行 `openflare-relay` 守护的 `tunnel_relay` 节点 |
| **穿透隧道 (Tunnel)** | 逻辑上的穿透客户端实例,拥有全局唯一 ID 与安全认证令牌,用以标识一个具体的内网环境。 | 由 Server 随机生成 `tunnel_id` (tun-<32hex>) |
| **隧道客户端 (Client)** | 运行在内网环境下的轻量控制器,根据 Server 下发的配置自动管理底层的 frpc 隧道子进程。 | 内网部署的 `openflared` 容器或独立二进制进程 |
| **隧道上游 (Tunnel Upstream)** | 网站配置中的特殊上游类型。选择此类型后,网关会将公网流量转发至本地中继端的 Vhost 端口,最终送达内网源站。 | 网站详情中配置的 `tunnel` 类型上游 |
---
## 推荐操作顺序
将一个内网服务发布到公网,推荐按这个顺序进行:
1. 注册并部署至少一个公网 **中继节点 (Relay)** 并保持在线。
2. 在管理端创建 **穿透隧道 (Tunnel)** 并复制对应的专属 Token。
3. 在内网服务器中部署并启动 **隧道客户端 (OpenFlared)**。
4. 确认管理端中该隧道的在线状态显示为「在线」。
5. 新增网站配置,上游类型选择 **内网穿透**,绑定对应隧道并填写内网端口(如 `127.0.0.1:8080`)。
6. 发布并激活新版本。
7. 通过公网域名访问,验证内网穿透链路是否打通。
---
## 详细配置步骤
### 第一步:准备中继节点 (Relay)
内网流量需要通过公网的中继节点进行中转。在开始前,你需要确保公网有一台可用的中继服务器。
1. 登录管理端,进入 **「节点管理」**。
2. 添加一个新节点,并将 **节点类型** 选择为 **中继节点 (tunnel_relay)**。
3. 保存后,复制该节点专属的 `agent_token`。
4. 在你的公网服务器上启动 `openflare-relay`。你可以直接使用 Docker 快速运行:
```bash
docker run -d --name openflare-relay --restart unless-stopped \
-p 7000:7000 \
-e OPENFLARE_SERVER_URL=http://<你的Server公网IP>:3000 \
-e OPENFLARE_AGENT_TOKEN=<刚才复制的AgentToken> \
-v openflare-relay-data:/var/lib/openflare-relay \
ghcr.io/rain-kl/openflare-relay:latest
```
> [!IMPORTANT]
> 请务必在云服务器安全组中放行 `7000` 端口(frpc 客户端连接控制端口)。如果你的 Server 与中继节点部署在同一台机器,这里的 `OPENFLARE_SERVER_URL` 应指向 Server 的公网或内网通信 IP。
### 第二步:在管理端创建穿透隧道
1. 导航至管理侧边栏的 **「内网穿透」** 页面。
2. 点击 **「创建隧道」** 按钮,在弹窗中填写:
* **隧道名称**:描述此内网环境,例如 `home-lab` 或 `office-dev`。
* **描述**:可选填,描述此隧道的具体用途。
3. 点击保存后,系统将自动生成该隧道的全局唯一 ID 与一串专属的 `tunnel_token`(形如 `tun-xxxx...`)。
4. 复制弹窗中为你生成的 **客户端部署命令**,用于下一步内网环境的部署。
### 第三步:部署内网客户端 (OpenFlared)
回到你的内网服务器中,根据刚才复制的部署命令运行客户端。
#### 方案 A:使用 Docker 部署(强烈推荐)
官方提供的 `openflared` 镜像已经内置了主控守护进程与 `frpc` 运行时,开箱即用,无需配置额外依赖:
```bash
docker run -d --name openflared --restart unless-stopped \
-e OPENFLARE_SERVER_URL=http://<你的Server公网IP>:3000 \
-e OPENFLARE_TUNNEL_TOKEN=<刚才复制的TunnelToken> \
-v openflared-data:/app/data \
ghcr.io/rain-kl/openflared:latest
```
#### 方案 B:宿主机二进制手动运行
如果你不便使用 Docker,也可以下载或自行编译 `flared` 二进制程序:
1. 在内网机器的程序同级目录下创建 `flared.json` 配置文件:
```json
{
"server_url": "http://<你的Server公网IP>:3000",
"tunnel_token": "<刚才复制的TunnelToken>",
"frpc_path": "/usr/local/bin/frpc",
"data_dir": "./data"
}
```
2. 执行启动命令:
```bash
./flared -config ./flared.json
```
#### 状态确认
启动成功后,内网客户端会通过出向网络向控制面发送心跳同步配置。此时:
1. 刷新管理端的 **「内网穿透」** 列表,刚才创建的隧道状态指示灯应当变为绿色的 **「在线」**。
2. 点击隧道详情,你可以直观地查看到当前内网客户端连接了公网的哪些中继 Relay 节点。
### 第四步:创建网站并绑定隧道上游
现在你可以为你的内网服务配置公网反向代理和域名访问了。
1. 进入 **「网站配置」** 页面,点击 **「新建网站」**。
2. 填写公网访问该网站所需的 **域名**,例如 `nas.example.com`。
3. 关键配置:在 **「上游配置」** 区域,将 **上游类型** 从默认的「直连」切换为 **「内网穿透」**。
4. 在下拉列表中选择你刚刚部署上线的 **内网隧道**(如 `home-lab`)。
5. 填写 **内网目标地址**(对于内网客户端来说可访问的本地地址与端口,例如 `127.0.0.1:8080`)与 **内网协议**(通常为 `http`)。
6. 配置其他站点常规项(如 TLS 证书等),并点击保存。
### 第五步:发布与生效
为了让网关的 OpenResty 能够正确匹配并路由域名流量,我们需要发布新的配置版本。
1. 点击导航栏右上角的 **「配置预览」**,确认生成的站点配置无误。
2. 在弹出窗口中,点击 **「发布并激活」**。
3. 此时,公网边缘的 Agent 会拉取到最新路由:它会将 `nas.example.com` 的请求转发至同机部署的 `openflare-relay (frps)` 的虚拟主机端口下。
4. 内网客户端 `openflared (frpc)` 会接收到被中继的封包,并安全地透传给内网的 `127.0.0.1:8080` 服务,最后原路返回响应。
5. 在你的公网浏览器中访问 `nas.example.com`,确认内网服务成功展示!
---
## 高级应用场景
### 1. 单隧道多服务复用 (多端口映射)
你并不需要为内网的每一个服务都部署一个 `openflared` 容器。
如果你想在一个内网环境映射多个不同的服务(例如:`127.0.0.1:80` 是博客,`127.0.0.1:8080` 是 API,`192.168.1.120:9000` 是内网网盘):
1. 保持这一个 `openflared` 客户端在线。
2. 在管理端创建三个独立的网站配置(绑定各自对应的公网域名)。
3. 这三个网站配置都将 **上游类型** 选为 **同一个穿透隧道**。
4. 分别在各自的“内网目标地址”中填入对应不同的端口或局域网 IP(例如 `127.0.0.1:80`、`127.0.0.1:8080`、`192.168.1.120:9000`)。
5. 发布并激活新版本,即可实现一隧多用。
### 2. 网关安全功能无缝叠加
因为所有公网流量均首先进入公网的 Agent 节点,在此处完成了 HTTPS/TLS 握手与 WAF 引擎拦截,然后再通过安全隧道送达内网。
因此,你的内网服务**天然且无需做任何改造**即可享受以下高级特性:
* **一键启用 HTTPS**:直接在管理端为域名选择或申请 SSL 证书,数据传输全程加密。
* **全局/自定义 WAF 防护**:开启 SQL 注入拦截、XSS 注入防御与恶意地域 IP 屏蔽。
* **人机挑战 (CC PoW)**:一键抵御针对内网服务的恶意 CC 刷接口攻击。
---
## 常见故障排查
### 1. 隧道在管理端显示为「离线」
* **检查 Token 是否正确**:查看 `flared` 日志或环境变量中配置的 `tunnel_token` 是否与管理端生成的一致。
* **检查网络连通性**:内网服务器需能通过出向网络正常请求 Server 地址。确保控制面没有启用防火墙限制客户端的 HTTP 请求。
* **中继节点防火墙未开**:检查对应中继节点的公网 `7000` 端口(或你自定义的 bindPort)是否已经在安全组中对公网放行。
### 2. 访问公网域名返回 502 Bad Gateway / 504 Gateway Timeout
* **内网服务未运行**:确认内网目标地址对应的服务已在内网服务器上成功启动并处于监听状态。
* **目标地址不可达**:如果内网地址填的是 `127.0.0.1:8080`,确保服务确实在运行着 `openflared` 的同一台主机上;如果填的是局域网 IP `192.168.x.x`,请在 `openflared` 容器内测试该局域网 IP 的连通性。
* **检查客户端应用日志**:在管理端查看「应用记录」或在内网查看 `flared` 运行日志,排查是否有 `LastError` 产生。frpc 在连不上内网端口时,会将连接失败报错原样上报至 Server 方便管理员定位。
### 3. 多中继网络动荡或重试失败
* 当控制面关联了多个 Relay 中继节点时,`openflared` 会为每个 Relay 独立派生 frpc 守护进程,并在 `flared.json` 中配置的 `sync_interval`(默认 30s)内定时向控制面拉取拓扑状态。
* 若发现某一中继节点频繁由于网络抖动离线,系统会自动触发退避重试机制。你可以在宿主机日志中看到 `frpc process missing, starting` 的日志,这属于正常的进程自愈逻辑,通常在网络恢复后 5~10 秒内即可自动恢复建连。
+21 -1
View File
@@ -63,6 +63,21 @@ OpenFlare 不直接在线修改节点上的 Nginx/OpenResty 配置。你在管
* 修改源站目录后,检查已发布的网站配置是否需要同步更新源站快照。
* 发布前使用预览或 diff 确认渲染结果。
## 托管 Pages 静态站点
Pages 用于托管已经构建完成的静态资源包。当前阶段只支持 Direct Upload,不执行 Git 构建、边缘函数或 SSR。
操作顺序:
1. 进入 **Pages** 页面,点击 **新建 Pages 项目**。
2. 填写项目名称、标识、描述;如为前端 history 路由应用,启用 **SPA fallback** 并填写回退路径,默认是 `/index.html`,也可以设置为 `/app.html` 等站点内绝对路径。
3. 创建后回到 Pages 项目列表,点击项目进入详情。
4. 在项目详情中上传 zip 静态资源包,并激活某个部署。
5. 新建或编辑网站规则,将回源方式切换为 **Pages 静态站点**,选择该 Pages 项目。
6. 发布并激活配置版本,Agent 会下载部署包、校验 checksum、解压到本地 Pages 目录,再由 OpenResty 本地服务静态文件。
Pages 项目只有在启用且存在激活部署后,才会出现在网站规则的 Pages 项目选择列表中。
## 启用 HTTPS
HTTPS 按域名绑定证书,而不是按整个网站统一强制启用。
@@ -89,6 +104,8 @@ HTTPS 按域名绑定证书,而不是按整个网站统一强制启用。
WAF 规则组、网站绑定或 PoW 配置修改后,需要重新发布并激活配置版本,Agent 才会拉取并应用到 OpenResty。IP 组成员变化不需要重新发布版本;在线 Agent 会通过 WebSocket 增量更新,离线或未升级 WS 的 Agent 会在下一次心跳中按 checksum 差异补齐。
详细的 WAF 安全配置与拦截判决原理请查阅 [WAF 安全防护使用](./waf-usage.md)。
## 发布、激活与回滚
标准链路:
@@ -126,6 +143,9 @@ WAF 规则组、网站绑定或 PoW 配置修改后,需要重新发布并激
5. 发布并激活版本。
6. 在 Agent 节点或浏览器访问域名验证。
> [!TIP]
> 如果你的源站部署在内网、没有公网 IP 且 Agent 无法直接访问,请使用内网穿透隧道功能将服务映射至公网。详细操作步骤请查阅 [内网穿透与隧道使用](./tunnel-usage.md)。
### 给已有域名启用 HTTPS
1. 准备覆盖该域名的证书。
@@ -144,7 +164,7 @@ WAF 规则组、网站绑定或 PoW 配置修改后,需要重新发布并激
## 推荐实践
* 生产环境显式配置 `SESSION_SECRET`,并优先使用 PostgreSQL。
* 生产环境必须显式配置 `JWT_SECRET`,并优先使用 PostgreSQL。
* 修改网站配置后先看预览或 diff,再发布。
* 每次发布后检查节点详情与应用记录。
* 多节点部署时保持 Agent 到 Server 的网络路径稳定。
+14 -7
View File
@@ -12,7 +12,7 @@
"rules": [
{
"name": "单 IP 404 高频扫描",
"expr": "request_count > 100 && status_404_ratio >= 0.8"
"expr": "request_count > 100 && StatusRatio(404) >= 0.8"
}
]
}
@@ -57,6 +57,13 @@ Host 是否为“通过 IP 访问”按请求日志中的 `Host` 字段判断:
比例字段都是 `0` 到 `1` 之间的小数。80% 应写成 `0.8`,50% 应写成 `0.5`。
### 自定义状态码匹配方法
如果内置的 `status_404_count` 和 `status_404_ratio` 不能满足您的需求,您可以使用以下内置方法来匹配任意状态码的请求数与占比:
* **`StatusCount(code)`**: 获取当前 IP 在回看窗口内返回指定状态码的请求数(如 `StatusCount(403) > 10`)
* **`StatusRatio(code)`**: 获取当前 IP 在回看窗口内返回指定状态码的请求数占该 IP 总请求数的比例(如 `StatusRatio(502) >= 0.5`)
## Expr 常用写法
自动 IP 组使用 Expr 语法,当前表达式必须返回布尔值。
@@ -67,12 +74,12 @@ Host 是否为“通过 IP 访问”按请求日志中的 `Host` 字段判断:
| --- | --- | --- |
| `>`、`>=`、`<`、`<=` | 数值比较 | `request_count > 100` |
| `==`、`!=` | 相等或不相等 | `ip != "127.0.0.1"` |
| `&&` | 并且 | `request_count > 100 && status_404_ratio >= 0.8` |
| `||` | 或者 | `status_404_ratio >= 0.8 || server_error_count > 20` |
| `&&` | 并且 | `request_count > 100 && StatusRatio(404) >= 0.8` |
| `||` | 或者 | `StatusRatio(404) >= 0.8 || server_error_count > 20` |
| `!` | 取反 | `!(ip == "127.0.0.1")` |
| `in` | 判断值是否在列表中 | `ip in ["203.0.113.10", "198.51.100.20"]` |
| `not in` | 判断值是否不在列表中 | `ip not in ["127.0.0.1"]` |
| `()` | 分组控制优先级 | `(request_count > 100 && status_404_ratio >= 0.8) || server_error_count > 50` |
| `()` | 分组控制优先级 | `(request_count > 100 && StatusRatio(404) >= 0.8) || server_error_count > 50` |
## 内置预设
@@ -81,7 +88,7 @@ Host 是否为“通过 IP 访问”按请求日志中的 `Host` 字段判断:
```json
{
"name": "单 IP 404 高频扫描",
"expr": "request_count > 100 && status_404_ratio >= 0.8"
"expr": "request_count > 100 && StatusRatio(404) >= 0.8"
}
```
@@ -106,7 +113,7 @@ Host 是否为“通过 IP 访问”按请求日志中的 `Host` 字段判断:
"rules": [
{
"name": "高频 404 扫描",
"expr": "request_count > 100 && status_404_ratio >= 0.8"
"expr": "request_count > 100 && StatusRatio(404) >= 0.8"
}
]
}
@@ -148,7 +155,7 @@ IP 直连访问异常:
"rules": [
{
"name": "排除可信 IP 的 404 扫描",
"expr": "ip not in [\"203.0.113.10\", \"198.51.100.20\"] && request_count > 100 && status_404_ratio >= 0.8"
"expr": "ip not in [\"203.0.113.10\", \"198.51.100.20\"] && request_count > 100 && StatusRatio(404) >= 0.8"
}
]
}
+162
View File
@@ -0,0 +1,162 @@
# WAF 安全防护使用
你会学到:OpenFlare 边缘 Web 应用防火墙 (WAF) 的工作原理、防护维度,如何管理与引用三类 IP 组(手动、订阅与基于 Expr 的自动 IP 组),配置防 CC 挑战(PoW 人机验证)与地域级拦截,以及如何在不 reload 进程的情况下实现 IP 组成员的秒级热更新。
---
## 核心概念
在配置安全策略前,你需要理解 WAF 的几个核心组成部分:
| 概念 | 说明 | 作用范围与生效方式 |
| --- | --- | --- |
| **WAF 规则组 (Rule Group)** | 安全规则的逻辑集合。包括:IP 黑白名单(直接录入或引用 IP 组)、国家/地区地域限制、防 CC 挑战(PoW)以及自定义拦截响应。 | 支持全局生效或绑定到单个/多个网站。**修改规则组定义必须发布并激活配置版本**。 |
| **IP 组 (IP Group)** | 存放单个 IP 或 CIDR 网段的列表容器。分为**手动**、**订阅**与**自动**三类。WAF 规则组可通过 ID 引用 IP 组。 | 属于动态资源。**IP 组成员的增减支持 WebSocket 秒级无缝热同步,无需 reload 进程**。 |
| **人机挑战 (CC PoW)** | 基于 Proof of Work (工作量证明) 的人机验证挑战。通过让浏览器计算特定难度的哈希碰撞,静默阻断恶意刷接口的自动化脚本与 Bot,保障正常用户体验。 | 位于规则组内的配置 Tab。**修改 PoW 参数必须发布并激活配置版本**。 |
---
## 推荐配置顺序
配置网站的安全防护时,推荐按这个顺序进行:
1. 进入 IP 组管理,创建所需的 **手动 IP 组** (如开发者白名单) 或 **自动 IP 组** (如根据 404 扫描自动封禁的 IP)。
2. 创建或编辑 **WAF 规则组**:
* 绑定需要引用或阻断的 IP 组。
* 配置国家或省份的地域黑白名单限制。
* (可选) 在 `PoW` 标签页配置人机挑战参数。
* 在 `拦截返回` 标签页设定自定义状态码(如 403, 418)和 HTML 拦截页。
3. 将规则组关联到对应的 **网站配置**。
4. 发布并激活配置版本,使边缘节点 (Agent) 开始应用 WAF 规则过滤流量。
---
## 详细步骤指南
### 第一步:管理与配置 IP 组
IP 组是进行大批量 IP 过滤的基石。OpenFlare 提供了极富弹性的三类 IP 组:
#### 1. 手动 IP 组 (Manual)
* **用途**:静态维护一些确定受信任或确定需长期拦截的 IP/网段。
* **配置**:点击「创建 IP 组」-> 类型选择「手动」-> 按行直接填入 IP 或 CIDR 格式(例如 `192.168.1.100` 或 `10.0.0.0/24`)。
#### 2. 订阅 IP 组 (Subscription)
* **用途**:接入第三方威胁情报库或云厂商公布的 IP 范围。
* **配置**:类型选择「订阅」-> 输入抓取 URL(支持按行分隔的文本文件或标准的 JSON 格式)。控制面板的定时任务会周期性拉取订阅源并自动同步至该组名单中。
#### 3. 自动 IP 组 (Automatic)
* **用途**:**最具杀伤力的防扫描、防爆破自动通道**。
* **配置**:类型选择「自动」-> 编写 Expr 日志聚合逻辑。你可以直接引用系统内置的预设:
* **单 IP 404 高频扫描**:`request_count > 100 && StatusRatio(404) >= 0.8` (单个 IP 最近一小时请求超 100 次且 404 响应占比超 80%)。
* **单 IP 直连访问异常**:`ip_host_count > 50 && ip_host_ratio > 0.5` (绕过域名直接通过 IP 地址进行高频请求)。
* **测试与立即执行**:保存前可点击 **「测试规则」** 按钮预览当前日志窗口被命中的 IP。保存后可点击 **「立即执行」** 直接聚合日志并生成封禁名单。
> [!TIP]
> 自动 IP 组的详细语法和可用指标请参阅 [WAF 自动 IP 组规则语法](./waf-ip-group-expr.md)。
---
### 第二步:创建与配置 WAF 规则组
1. 导航至左侧菜单 **「安全防护 (WAF)」**,点击 **「创建规则组」**。
2. 填写规则组名称(如 `production-api-shield`),选择是否为「全局规则组」。
3. 进入规则组详情,在下方几个配置 Tab 中依次设置:
#### 1. 黑白名单配置 (Allow / Block Lists)
* **直录 IP**:可直接在框内按行填入临时需要白名单放行或黑名单阻断的单个 IP 或网段。
* **IP 组引用**:点击「绑定 IP 组」,选择你在第一步中配置好的手动、自动或订阅 IP 组。白名单引用会直接放行,黑名单引用则直接阻断。
#### 2. 地域限制 (GeoIP)
* **说明**:OpenFlare 集成了 GeoIP 地理位置解析。
* **配置**:可开启地域限制开关,模式可选择「仅允许」或「禁止」。
* * 例如,若你的服务只服务于国内,可以将模式设为「仅允许」,并在国家列表中勾选 `中国`。
* * 支持细化到具体省份/地区(Region),一键拦截特定地理区域的恶意流量。
#### 3. 人机挑战配置 (PoW CC 防护)
* **说明**:开启防 CC 的人机挑战。当请求触发防CC机制时,浏览器会渲染一个静默挑战页面,并在几百毫秒内完成数学计算(哈希碰撞)。通过后会被写入 Cookie,后续访问直接放行。此过程对真实用户几乎无感,但能完美拦截不支持 JS/不具备计算能力的爆破脚本与 CC 僵尸工具。
* **核心参数**:
* **开启状态**:启用/禁用。
* **哈希难度**:控制碰撞难度(建议设定为 `4` 或 `5`)。
* **Cookie 有效期**:挑战通过后,在多长时间内免验证(例如 `3600` 秒)。
* **自定义挑战 HTML**:可定制挑战中的 Loading 页面风格,让其融入你的业务设计。
#### 4. 拦截返回 (Block Response)
* **说明**:设定 WAF 规则拦截恶意请求时的返回行为。
* **配置**:
* **拦截状态码**:可自定义拦截响应的 HTTP 状态码,例如标准的 `403`,或带有趣味性质的 `418 (I'm a teapot)`。
* **拦截响应体**:可在此输入自定义的 HTML 内容,展示给被拦截的攻击者(如:“WAF 拦截:你的请求已被记录”)。
---
### 第三步:将规则组关联到网站
规则组配置完成后,并不会自动生效,你需要将其与具体的网站配置绑定。
* **方案 A (推荐)**:在规则组详情页面的 **「绑定网站」** 选项卡中,一键勾选你希望启用此防护的网站并保存。
* **方案 B**:回到 **「网站配置」** 中编辑某个具体网站,在其「安全防护」配置区,勾选并绑定刚才创建的规则组。
> [!NOTE]
> 如果规则组被标记为 **「全局规则组 (is_global)」**,它将自动应用到网关上托管的**所有网站**,无需手动执行绑定。
---
### 第四步:发布并生效配置
1. 如果你修改了 **规则组定义**、**GeoIP 范围**、**PoW 防CC难度** 或 **网站的绑定关系**:
* 你需要点击管理端右上角的 **「配置预览」** -> **「发布并激活」**。
* Agent 拉取并校验新版本后,将重写本地 OpenResty 核心配置文件(`waf_config.json` 等)并平滑重载进程使策略生效。
2. 如果你只是更新了 **IP 组的成员名单**(如:在手动 IP 组中删减了一个 IP,或者自动 IP 组定时聚合出了一批新的封禁 IP):
* **不需要做任何发布操作!**
* Server 会在数据库更新后立即计算 IP 组全新的 Checksum 摘要。
* 控制面会通过 **WebSocket 长连接实时向所有在线的 Agent 广播** 变更的 IP 组成员,Agent 接收后会增量覆写到本地的运行时磁盘文件 `waf_ip_groups.json`。
* OpenResty Lua 引擎在处理新请求时,会在微秒级计算文件哈希,若发现 Checksum 变更则实时重载入内存字典(`ngx.shared`),**整个过程全程不需要 reload 任何 Nginx 服务,对线上高并发业务毫无影响**。
* 即使 WebSocket 连接意外中断,Agent 也会在每周期心跳中上报本地 Checksum,由 Server 差分补齐下发,确保万无一失。
---
## WAF 判定逻辑 (过滤漏斗)
当一个外部请求到达 OpenResty 数据面时,WAF 运行时引擎会以微秒级的极速开销进行如下判决流检测。只要判定出明确结果,即不再向下执行:
```text
请求进入 access 阶段
│
▼
获取当前请求绑定的所有规则组 (全局规则组 + 自定义规则组)
│
▼
1. 匹配 IP 白名单 / 白名单 IP 组? ──────(是)─────► [ 放行 (ALLOW) ]
│ (否)
▼
2. 匹配国家 / 省份地域白名单? ────────(是)─────► [ 放行 (ALLOW) ]
│ (否)
▼
3. 匹配 IP 黑名单 / 黑名单 IP 组? ──────(是)─────► [ 拦截 (BLOCK) ] ──► 返回自定义状态码与HTML拦截页
│ (否)
▼
4. 匹配国家 / 省份地域黑名单? ────────(是)─────► [ 拦截 (BLOCK) ] ──► 返回自定义状态码与HTML拦截页
│ (否)
▼
5. 该站点是否启用了 PoW CC 防护?
├───(是)───► [ 校验 PoW Cookie ] ──(验证通过)──► [ 放行 (ALLOW) ]
│ │
│ (未通过)
│ ▼
│ [ 渲染 PoW 挑战页 ] ──(计算正确)──► 写入 Cookie 并放行
▼
6. 未触发任何策略,属于正常业务流量 ───────────────► [ 放行 (ALLOW) ]
```
---
## 最佳实践与调优建议
* **白名单前置与保护**:在部署高强度黑名单或地域屏蔽前,建议首先创建一个「受信任 IP 组」,放入你团队的办公室出口 IP、本地开发 IP 以及可能访问你的第三方回调源站 IP(如微信、支付宝支付回调地址),并在规则组的**白名单**中优先引入。这可以有效防止误杀。
* **合理微调 PoW 难度**:人机 CC 挑战的哈希碰撞计算(`challenge_difficulty`)是一把双刃剑。
* 难度值 `3`:几乎瞬间完成计算,防 CC 强度低。
* 难度值 `4`:普通手机/低端浏览器在 100~300ms 内完成计算,防护性能良好。
* 难度值 `5`:需要 500ms~2s,防护性强,但低配端可能会感觉稍显卡顿。
* 难度值 `6` 及以上:计算量呈指数级上升,可能导致移动端用户浏览器 CPU 持续打满卡死。**因此强烈建议在生产环境选用 `4` 或 `5`**。
* **善用“测试规则”**:对于自动 IP 组,在点击保存之前务必点击 **「测试规则」**。通过分析当前窗口内被命中的 IP 列表,确认你的 Expr 表达式阈值(如请求数、404占比等)配置是否过宽或过紧,防止由于阈值配置不合理导致大面积误封正常用户。
* **分离静态与动态黑名单**:不要将需要长期封禁的静态恶意 IP 填入自动封禁组(因为自动聚合的名单随时会被新的执行窗口覆盖)。应该将确定的恶意 IP 录入到一个专门的「手动封禁 IP 组」中,并让规则组同时引用该手动组与自动组。
+34 -36
View File
@@ -2,22 +2,7 @@
你会学到:OpenFlare 代码修改的准入标准、后端/Agent/前端分层约束、数据模型边界、API 约定、数据库迁移要求和测试交付基线。
本文档融合原开发规范、前端规范与开发计划,是 OpenFlare `1.0.0` 之后的工程约束入口。
## 当前结论
* 第一版至第六版的主线能力已经全部完成。
* `1.0.0` 是当前正式基线。
* 已完成阶段的过程性任务以代码、测试与 Git 历史为准。
* 新工作优先以缺陷修复、可维护性改进、文档与测试补强为主。
当前开发优先级:
1. 稳定性。
2. 升级与回滚链路可靠性。
3. 文档准确性。
4. 测试覆盖补强。
5. 在既有边界内的小步迭代。
本文档融合原开发规范、前端规范与开发计划。
## 变更准入
@@ -30,14 +15,6 @@
如果需求超出边界或引入新基础设施,应先更新设计文档,再开始实现。
任何合入正式基线的改动,至少应满足:
* 不破坏 Agent 心跳、同步、发布与回滚主链路。
* 不破坏现有 OpenResty 主配置托管模型。
* 不降低总览、节点详情与访问分析的既有可用性。
* 有与风险相称的测试或联调验证。
* 文档与代码保持一致。
## 技术基线
Server:
@@ -72,7 +49,9 @@ Frontend:
各组件和模块(Server、Agent、Frontend)的物理目录分层职责详见 [仓库结构](../design/repository.md)。在此结构下,开发必须遵守以下核心分层规则:
* **Server 开发规则**:禁止在 `controller/` 堆积业务逻辑,禁止在 `middleware/` 实现业务流程,禁止为简单需求新增平台层抽象。
* **Server 开发规则**:
* 禁止在 `controller/` 堆积业务逻辑,禁止在 `middleware/` 实现业务流程,禁止为简单需求新增平台层抽象。
* **定时任务开发规则**:禁止将不同业务模块(如 Uptime Kuma 整合、WAF IP 同步等)的定时任务具体执行逻辑与状态堆积在单个 `cron.go` 文件中。各模块对应的定时任务结构体和运行逻辑必须在独立的 Go 文件中定义,`cron.go` 只允许承担统一注册、初始化与调度器启停的职责。
* **Agent 开发规则**:每个模块职责单一,外部命令调用集中封装,状态落盘与配置落盘分离。
* **Frontend 开发规则**:页面文件只负责获取路由参数、组织页面结构、调用 feature 组件;不应手写复杂 API 细节、复杂表单校验逻辑或维护大量彼此耦合的局部状态。
@@ -82,6 +61,7 @@ Frontend:
### 1. 当前有效实体
* **核心配置与反代**:`proxy_routes` (网站配置), `origins` (源站), `config_versions` (配置版本), `tls_certificates` (证书), `managed_domains` (托管域名).
* **Pages 静态托管**:`pages_projects` (Pages 项目), `pages_deployments` (不可变部署), `pages_deployment_files` (部署文件清单).
* **节点与状态**:`nodes` (节点), `node_system_profiles` (系统概况), `apply_logs` (应用日志).
* **内网穿透**:`tunnels` (隧道客户端), `tunnel_tokens` (隧道认证令牌,可选持久化).
* **观测与分析**:`node_request_reports` (请求上报), `node_access_logs` (访问明细), `node_metric_snapshots` (指标快照), `traffic_analytics_rollups` (流量聚合), `node_health_events` (健康事件).
@@ -115,6 +95,13 @@ Frontend:
* 必须指定 `tunnel_target_addr`(内网目标地址,如 `192.168.1.100:8080`)和 `tunnel_target_protocol`(`http` 或 `https`)。
* 发布配置时,Server 自动将此上游渲染为 `http://127.0.0.1:{relay_vhost_port}`,Agent 依据 Host 头由 frps 路由。
* **Pages 与上游关联**:
* `proxy_routes.upstream_type = 'pages'` 时,必须指定 `pages_project_id`。
* 被引用的 Pages 项目必须启用,且必须存在当前激活部署。
* Pages 项目可启用 SPA fallback 并配置站点内绝对回退路径(默认 `/index.html`);回退路径必须经 Server 校验后进入发布快照,不得直接拼接未校验输入到 OpenResty 配置。
* Pages 部署包必须作为 Server 本地文件保存,数据库只保存部署元数据与文件清单;禁止把静态资源内容写入 `config_versions.support_files_json`。
* 发布配置时,Server 将 Pages 上游渲染为 OpenResty `root` + `try_files` 静态服务,并保留网站规则已有的 HTTPS、WAF、PoW、Basic Auth、限流与缓存配置。
* **TunnelRelay 节点配置**:
* `nodes.node_type = 'tunnel_relay'` 时,新增字段 `relay_bind_port`、`relay_vhost_http_port`、`relay_auth_token` 必须有合理默认值。
* `relay_bind_port` 默认 7000,`relay_vhost_http_port` 默认 8080。
@@ -135,17 +122,19 @@ Frontend:
每次提升数据库版本号时,必须补充从上一版本升级到新版本的显式迁移方法。迁移方法必须包含升级后的校验逻辑;只有校验通过,才能写入新的数据库版本记录。
v1-v7 视为历史初始基线,不再维护逐版本升级文件。从 v8 起,数据库迁移必须放在 `openflare_server/model/migrate` 目录中,并以目标版本命名文件,例如 `v16.go`。每个版本文件通过 `init()` 注册自己的迁移,当前数据库版本取已注册迁移的最大目标版本。不得为了整理文件而改变已发布 v8+ 迁移的语义。
v1-v7 视为历史初始基线,不再维护逐版本升级文件。v8-v17 是旧升级框架的兼容迁移链,只保留在 `openflare_server/model/migrate` 目录中用于老库升级。旧库启动时必须先按旧框架升级到 `legacyMigrationTerminalVersion`,再桥接到 goose;不得为了整理文件而改变已发布 v8-v17 迁移的语义。
从 v17 之后,数据库升级统一使用 goose。新的 goose provider、桥接逻辑、注册入口和具体迁移文件必须全部放在 `openflare_server/model/goose` 包下,`openflare_server/model` 根包只保留纯净实体类、旧框架兼容适配和必要的上下文注入。每次新增数据库升级都必须新建一个单独的 Go 文件,文件名使用 `openflare_server/model/goose/goose_<timestamp>_<description>.go`,例如 `openflare_server/model/goose/goose_202606020001_add_node_capabilities_json.go`。迁移文件必须同时包含该版本的 goose migration 构造函数、升级逻辑和校验逻辑;`model/goose/migrations.go` 只能作为注册入口和公共构造工具,禁止把具体迁移逻辑集中堆放在该文件中。
执行数据库升级时必须按以下步骤完成:
1. 判断是否需要升级数据库版本:凡是新增/删除/重命名表、字段、索引、约束、列类型、分表规则,或改变持久化数据语义,都必须升级。
2. 新增 `openflare_server/model/migrate/vN.go`,其中 `N` 为目标版本号。文件头部必须包含注释,说明本次升级了什么内容,以及为什么需要升级。
3. 在 `vN.go` 中实现 `VN()`,并在 `init()` 中调用 `Register(VN())`。`FromVersion` 必须等于 `N-1`,`ToVersion` 必须等于 `N`。
4. 在 `migrateVN` 中写入升级逻辑。可通过 `Context` 调用 `ApplyCurrentSchema`、历史 backfill、默认数据初始化等公共能力;复杂数据修复必须显式处理,不得只依赖 `AutoMigrate`。
5. 在 `validateVN` 中写入升级后的校验逻辑。校验至少要覆盖新增表/字段/索引是否存在、关键默认数据是否存在、必要的数据回填是否成功。
6. 如果新迁移需要新的公共 backfill 或校验辅助函数,将其放在 `openflare_server/model/migrations.go` 或更合适的 model 文件中,并通过 `Context` 暴露给 `model/migrate`,避免子包反向 import `model` 造成循环依赖。
7. 补充迁移测试:至少覆盖从 `N-1` 老库升级到 `N` 后 schema version、字段/表结构、关键数据回填和校验结果。注册表连续性由 `model/migrate` 测试兜底,但具体业务迁移仍必须有测试。
2. 新增 `openflare_server/model/goose/goose_<timestamp>_<description>.go`,其中 `<timestamp>` 为 goose 版本号。文件头部或迁移构造函数附近必须包含注释,说明本次升级了什么内容,以及为什么需要升级。
3. 在该文件中实现独立迁移构造函数,并返回通过 `newGORMMigration(...)` 创建的 migration;随后只在 `openflare_server/model/goose/migrations.go` 的 `registeredMigrations(...)` 中新增一条注册项。
4. 在同一个单独迁移文件中写入升级逻辑。可通过 goose `Context` 调用 `ApplyCurrentSchema`、历史 backfill、默认数据初始化等公共能力;复杂数据修复必须显式处理,不得只依赖 `AutoMigrate`。
5. 在同一个单独迁移文件中写入升级后的校验逻辑。校验至少要覆盖新增表/字段/索引是否存在、关键默认数据是否存在、必要的数据回填是否成功。
6. 如果新迁移需要新的公共 backfill 或校验辅助函数,优先放在该迁移文件中;只有多个迁移共同复用时,才放到 `openflare_server/model/goose` 包内的公共文件中。不要把新 goose 框架代码放回 `openflare_server/model` 根包。
7. 补充迁移测试:至少覆盖从旧框架终点或上一 goose 版本升级后 schema version、字段/表结构、关键数据回填和校验结果。还应保留旧库从 v15/v17 桥接到 goose 的回归覆盖。
8. 同步更新设计/开发文档;如果管理端 API、配置项或用户可见行为变化,还要同步更新对应指南、配置参考和 Swagger 文档。
新包启动后必须先检查数据库当前版本,再按顺序逐步升级到目标版本;禁止跳过中间升级步骤直接写目标版本。
@@ -178,14 +167,19 @@ v1-v7 视为历史初始基线,不再维护逐版本升级文件。从 v8 起
- Client 心跳返回 tunnel 配置版本摘要。
- Client 可拉取完整配置(relay 列表 + frpc 代理定义)。
- Client 上报配置应用结果。
* **Admin Tunnel 管理 API** - `/api/tunnels/*`,要求 Admin Session。
* **Admin Tunnel 管理 API** - `/api/tunnels/*`,要求管理端 `OPENFLARE_TOKEN`。
- CRUD tunnel 实体(创建、查询、更新、删除)。
- Token 管理(生成、轮换)。
- 强制同步(触发 Client 立即拉取新配置)。
* **Admin Pages 管理 API** - `/api/pages/*`,要求管理端 `OPENFLARE_TOKEN`。
- CRUD Pages 项目,包括 SPA fallback 启用状态与回退路径。
- 上传 zip 部署包、查看部署历史、激活部署、删除非激活部署。
* **Agent Pages 下载 API** - `/api/agent/pages/*`,使用 `X-Agent-Token` 认证。
- Agent 仅能按激活配置引用的部署 ID 拉取静态部署包,不提供任意文件读取或远程命令入口。
* 总览与节点详情优先使用专用聚合接口。
* 管理端变更类接口统一使用 `POST`;只读接口使用 `GET`。
* 管理端继续复用现有登录、角色与 Session。
* 第三方登录统一通过认证源 API 进入,认证源管理接口必须要求 Root Session。
* 管理端登录成功后返回用户 token;管理端 API 只允许从 `OPENFLARE_TOKEN` 请求头读取登录凭证,不得通过 Cookie Session 放行。
* 第三方登录统一通过认证源 API 进入,认证源管理接口必须要求 Root 级 `OPENFLARE_TOKEN`。
* `/api/status` 只能返回已启用认证源的公开字段,不得返回 Client Secret。
* 第三方账号未绑定且注册关闭时,应提供绑定已有账号流程,不得自动创建用户。
* Agent/Relay/Client 正式请求统一使用对应的专属 token(`agent_token` / `relay_token`(即 agent_token) / `tunnel_token`)。
@@ -203,6 +197,9 @@ v1-v7 视为历史初始基线,不再维护逐版本升级文件。从 v8 起
* 读取 WAF 规则组、规则组引用的 IP 组与网站绑定关系,并在发布快照中保存可回放数据。
* 自动型 WAF IP 组只能由 Server 定时任务读取请求日志并执行 Expr 布尔规则,OpenResty Lua 与 Agent 不得直接访问请求日志库或执行自动挖掘逻辑。
* 发布版本不得展开 WAF IP 组成员;Agent 必须通过独立的 IP 组 checksum 差异同步和 WebSocket 增量广播维护本地 `waf_ip_groups.json`。
* **Pages 配置扩展**:区分上游类型,为 `upstream_type = 'pages'` 的代理规则生成 Pages 部署快照。
* OpenResty 侧:将 Pages 上游渲染为本地静态目录 `root` 与 `try_files`,启用 SPA fallback 时使用项目配置的回退路径,不得渲染 `proxy_pass`。
* Agent 侧:在应用 OpenResty 配置前,必须确保引用的 Pages 部署包已下载、checksum 校验通过并解压到 `pages_dir`。
* **内网穿透配置扩展**:区分上游类型,为 `upstream_type = 'tunnel'` 的代理规则生成独立的 tunnel 配置数据。
* OpenResty 侧:将 tunnel 上游自动渲染为 `http://127.0.0.1:{relay_vhost_port}`,必须保留原始 `Host` 请求头。
* Tunnel 侧:为每个 Client 生成完整的 relay 列表与 frpc 代理定义(frpc proxy 配置)。
@@ -228,6 +225,7 @@ Agent 必须满足:
* 发现新版本时先备份旧文件。
* 写入主配置、路由配置与必要证书文件。
* 写入 WAF/PoW 运行时配置,并确保 WAF Lua 资源由 Agent 统一管理。
* 如果激活配置引用 Pages 部署,必须通过 Agent API 拉取部署包,校验配置快照中的 SHA-256 checksum,安全解压并原子切换本地当前部署目录;zip 路径不得逃逸 `pages_dir`,不得接受符号链接。
* WAF IP 组同步必须按组增量更新,不得在每次心跳或每次同步中传输全部 IP 组。
* 写入新配置后执行 `openresty -t -c <main_config_path>`,再 reload;reload 发现运行时未启动时允许直接启动 OpenResty。
* 周期性运行时健康检查不得调用 `openresty -t`,避免健康探针触发 upstream 域名同步解析;应优先请求本地 `openresty_observability_port` 上的 `/openflare/stub_status`,以 HTTP `200 OK` 作为 OpenResty 主进程和 worker 正在提供服务的判断依据。
@@ -303,7 +301,7 @@ OpenFlared 必须满足:
* 产品边界变动:更新 [产品边界](../design/index.md)。
* 工程约束变动:更新本文档。
* 部署与配置变动:更新 [部署说明](../reference/deployment.md)、[配置项](../reference/configuration.md) 与 README。
* 部署与配置变动:更新 [部署说明](../deployment/deployment.md)、[配置项](../reference/configuration.md) 与 README。
如果未来出现明确的新阶段目标,再单独新增专项计划文档;不要把已完成的历史计划继续堆回本文档。
+17 -14
View File
@@ -3,8 +3,8 @@ layout: home
hero:
name: OpenFlare
text: 自托管 OpenResty 控制面
tagline: 管理反向代理规则、配置发布、节点同步、TLS 证书与基础观测。
text: 开源 CDN 编排与边缘安全平台
tagline: 支持反向代理、集中式配置同步、内网穿透(Tunnels)、动态 WAF 防护与人机防 CC 挑战。
actions:
- theme: brand
text: 快速开始
@@ -17,16 +17,19 @@ hero:
link: https://github.com/Rain-kl/OpenFlare
features:
- icon: 🧭
title: 统一控制面
details: 在一个管理端维护网站、域名、源站、证书、节点与版本状态。
- icon: 🚀
title: 不可变发布
details: 每次发布生成完整 OpenResty 配置快照,可预览、激活和回滚。
- icon: 🔁
title: Agent 自动应用
details: 节点侧自动拉取、校验、reload,并在失败时回滚到可运行配置。
- icon: 📊
title: 基础观测
details: 提供请求聚合、访问分析、资源快照、健康事件与节点详情。
- icon: 🛰️
title: 集中式配置同步
details: 通过 WebSocket 与心跳实现全网节点配置秒级同步下发与热生效,状态即时回收。
- icon: 🌐
title: 分布式 CDN 编排
details: 将独立的 OpenResty 编排为高度协同的分布式 CDN 舰队,支持源站多负载均衡。
- icon: 🚇
title: 安全内网穿透 (Tunnels)
details: 对标 Cloudflare Tunnels,无须公网 IP 或暴露入向端口,安全穿透本地服务至公网。
- icon: 🛡️
title: 边缘 WAF 安全防护
details: IP 组成员差分同步写入 Lua 共享内存,实现免 Nginx 重载的 WAF 热更新与 GeoIP 过滤。
- icon: 🧩
title: 防 CC 与人机挑战 (PoW)
details: 内置高性能客户端 Proof of Work 密码学挑战,网关边缘秒级拦截阻断僵尸网络与爬虫。
---
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: true
+10 -20
View File
@@ -20,7 +20,7 @@ OpenFlare 的管理端 API 与 Agent API 都使用 JSON。
| 类型 | 约定 |
| --- | --- |
| 管理端 API | 由管理端 Session 鉴权 |
| 管理端 API | 由 `OPENFLARE_TOKEN` 请求头鉴权 |
| Agent API | 固定放在 `/api/agent/*` |
| Relay API | 固定放在 `/api/relay/*`,使用 `X-Agent-Token` 鉴权(与 Agent 复用同一 token) |
| OpenFlared API | 固定放在 `/api/flared/*`,使用 `X-Tunnel-Token` 鉴权(独立的 tunnel_token) |
@@ -29,7 +29,7 @@ OpenFlare 的管理端 API 与 Agent API 都使用 JSON。
## WAF IP 组接口
管理端 WAF IP 组接口统一要求管理端 Session 鉴权:
管理端 WAF IP 组接口统一要求管理端 `OPENFLARE_TOKEN` 鉴权:
| 方法 | 路径 | 说明 |
| --- | --- | --- |
@@ -41,29 +41,19 @@ OpenFlare 的管理端 API 与 Agent API 都使用 JSON。
| `POST` | `/api/waf/ip-groups/:id/delete` | 删除 IP 组;已被规则组引用时会拒绝 |
| `POST` | `/api/waf/ip-groups/:id/sync` | 立即同步订阅型 IP 组或立即执行自动型 IP 组 |
IP 组 `type` 支持 `manual`、`automatic`、`subscription`。自动型 IP 组的 `auto_config` 是 JSON 对象,当前支持:
```json
{
"lookback_minutes": 60,
"rules": [
{
"name": "单 IP 404 高频扫描",
"expr": "request_count > 100 && status_404_ratio >= 0.8"
},
{
"name": "单 IP 直连访问异常",
"expr": "ip_host_count > 50 && ip_host_ratio > 0.5"
}
]
}
```
IP 组 `type` 支持 `manual`、`automatic`、`subscription`。自动型 IP 组的 `auto_config` 是 JSON 对象
自动规则使用 Expr 语法,表达式必须返回布尔值。规则按单个 IP 的请求日志聚合指标计算,可用字段包括 `ip`、`request_count`、`status_404_count`、`status_404_ratio`、`ip_host_count`、`ip_host_ratio`、`client_error_count`、`server_error_count`、`last_seen_unix`。完整语法和字段含义见 [WAF 自动 IP 组规则语法](../guide/waf-ip-group-expr.md)。订阅格式支持 `text` 与 `json`:文本格式按行解析 IP/IP 段并忽略空行和 `#` 开头的注释;JSON 格式可通过映射规则选择数组,默认读取根数组。
## 鉴权
管理端继续复用现有登录、角色与 Session。
管理端登录成功后返回用户 token,后续所有管理端 API 必须在请求头中携带:
```http
OPENFLARE_TOKEN: <token>
```
Server 只从 `OPENFLARE_TOKEN` 读取管理端登录凭证,不再通过 Cookie Session 放行管理端 API。角色和用户状态仍以数据库中的当前用户记录为准。
Agent 正式请求统一使用节点专属 `agent_token`,首次接入可使用全局 `discovery_token`。Agent 请求头固定为:
+34 -1
View File
@@ -8,7 +8,7 @@
```bash
cd openflare_server
export SESSION_SECRET='replace-with-random-string'
export JWT_SECRET='replace-with-random-string'
export SQLITE_PATH='./openflare.db'
export LOG_LEVEL='info'
go run .
@@ -76,6 +76,39 @@ cd openflare_agent
GOCACHE=/tmp/openflare-go-cache go test ./...
```
## Relay (中继端)
源码运行:
```bash
cd openflare_relay
go run ./cmd -config /path/to/relay.json
```
编译:
```bash
cd openflare_relay
go build -o openflare-relay ./cmd
```
## OpenFlared (Client 客户端)
源码运行:
```bash
cd openflared
go run ./cmd -config /path/to/flared.json
```
编译:
```bash
cd openflared
go build -o openflared ./cmd
```
## 安装 Agent
```bash
+135 -7
View File
@@ -16,7 +16,19 @@ Agent 支持:
1. `-config` 命令行参数。
2. `agent.json` 配置文件。
3. 少量日志相关环境变量。
3. 少量日志与配置覆盖相关环境变量。
Relay (中继端) 支持:
1. `-config` 命令行参数。
2. `relay.json` 配置文件。
3. 丰富的启动覆盖环境变量。
Client (内网客户端) 支持:
1. `-config` 命令行参数。
2. `flared.json` 配置文件。
3. 启动覆盖与日志环境变量。
## 配置文件位置
@@ -26,6 +38,10 @@ Agent 支持:
| Agent 配置文件 | `./agent.json` | 可通过 `-config` 指定 |
| 一键安装 Agent 配置 | `/opt/openflare-agent/agent.json` | 安装脚本默认生成 |
| Agent 数据目录 | 配置文件所在目录下的 `data` | 可通过 `data_dir` 修改 |
| Relay 配置文件 | `./relay.json` | 可通过 `-config` 指定 |
| 一键安装 Relay 配置 | `/opt/openflare-relay/relay.json` | 安装脚本默认生成 |
| Client 配置文件 | `./flared.json` | 可通过 `-config` 指定 |
| 一键安装 Client 配置 | `/opt/openflared/flared.json` | 安装脚本默认生成 |
## Server 命令行参数
@@ -48,7 +64,7 @@ go run . --port 3000 --log-dir ./logs
| `PORT` | Server 监听端口 | `3000` |
| `GIN_MODE` | Gin 运行模式 | 非 `debug` 时按 release |
| `LOG_LEVEL` | 日志等级 | `info` |
| `SESSION_SECRET` | Session 签名密钥 | 启动时随机生成 |
| `JWT_SECRET` | 管理端 API 登录令牌的 JWT 签名密钥,生产环境必须显式配置 | 启动时随机生成 |
| `SQLITE_PATH` | SQLite 数据库文件路径 | `openflare.db` |
| `DSN` | PostgreSQL DSN,设置后优先于 SQLite | 空 |
| `SQL_DSN` | 兼容旧命名的 PostgreSQL DSN,优先级低于 `DSN` | 空 |
@@ -60,7 +76,7 @@ go run . --port 3000 --log-dir ./logs
* `DSN` 与 `SQL_DSN` 同时存在时优先使用 `DSN`。
* `DSN` 或 `SQL_DSN` 与 `SQLITE_PATH` 同时存在时优先使用 PostgreSQL。
* 当目标 PostgreSQL 数据库为空且本地 `SQLITE_PATH` 文件存在时,Server 启动阶段会自动迁移 SQLite 数据,并在日志中输出按表迁移进度。
* `SESSION_SECRET` 生产环境必须显式配置。
* `JWT_SECRET` 用于管理端 API 登录令牌的签名与验证,生产环境必须显式配置,避免重启后所有已登录令牌失效。
* `REDIS_CONN_STRING` 未配置时,相关能力回退为进程内实现。
## 运行时 Option
@@ -79,6 +95,17 @@ go run . --port 3000 --log-dir ./logs
| `GlobalApiRateLimitNum` / `GlobalApiRateLimitDuration` | 全局 API 限流次数 / 时间窗口 | `300` / `180` |
| `GlobalWebRateLimitNum` / `GlobalWebRateLimitDuration` | 全局 Web 限流次数 / 时间窗口 | `300` / `180` |
| `CriticalRateLimitNum` / `CriticalRateLimitDuration` | 敏感接口限流次数 / 时间窗口 | `100` / `1200` |
| `UptimeKumaEnabled` | 是否启用 Uptime Kuma 自动同步 | `false` |
| `UptimeKumaUrl` | Uptime Kuma 实例地址 | 空 |
| `UptimeKumaUsername` | Uptime Kuma 登录用户名 | 空 |
| `UptimeKumaPassword` | Uptime Kuma 登录密码(写专,接口不回显) | 空 |
| `UptimeKumaMonitorScope` | 监控范围,支持 `all` (全部站点) 或 `selected` (选择站点) | `all` |
| `UptimeKumaSelectedSites` | 已选择监控站点的名称列表(英文逗号分隔) | 空 |
| `UptimeKumaSyncInterval` | 自动差分同步间隔(分钟) | `5` |
| `UptimeKumaInterval` | 监控心跳检测频率(秒) | `60` |
| `UptimeKumaRetry` | 监控最大重试次数 | `0` |
| `UptimeKumaRetryInterval` | 监控重试间隔时间(秒) | `60` |
| `UptimeKumaTimeout` | 监控请求超时断开时间(秒) | `48` |
说明:
@@ -141,6 +168,7 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前
| `OPENFLARE_NODE_IP` | 节点 IP,可覆盖 `agent.json` | 空 |
| `OPENFLARE_DATA_DIR` | Agent 数据目录,可覆盖 `agent.json` | 空 |
| `OPENFLARE_OPENRESTY_PATH` | OpenResty 二进制路径,可覆盖 `agent.json` | 空 |
| `OPENFLARE_PAGES_DIR` | Pages 静态部署目录,可覆盖 `agent.json` | 空 |
| `OPENFLARE_HEARTBEAT_INTERVAL` | 心跳间隔,可覆盖 `agent.json` | 空 |
| `OPENFLARE_REQUEST_TIMEOUT` | 请求超时,可覆盖 `agent.json` | 空 |
| `OPENFLARE_OPENRESTY_OBSERVABILITY_PORT` | 本地观测端口,可覆盖 `agent.json` | 空 |
@@ -174,6 +202,7 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前
| `lua_dir` | Lua 脚本与静态资源写入目录 | 否 | `data_dir/etc/nginx/lua` |
| `openresty_lua_dir` | OpenResty 配置中读取 Lua 的目录 | 否 | 同 `lua_dir` |
| `runtime_config_dir` | Agent 运行时配置写入目录,如 `pow_config.json` | 否 | `data_dir/etc/openflare` |
| `pages_dir` | Pages 静态部署包解压与当前部署目录 | 否 | `data_dir/var/lib/openflare/pages` |
| `mmdb_path` | WAF GeoIP mmdb 文件路径 | 否 | `data_dir/etc/openflare/GeoLite2-Country.mmdb` |
| `mmdb_update_interval` | WAF GeoIP mmdb 更新间隔 | 否 | `86400000` 毫秒 |
| `mmdb_download_url` | WAF GeoIP mmdb 下载地址 | 否 | 内置 GeoLite2 Country 下载地址 |
@@ -191,17 +220,82 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前
* 未配置 `openresty_path` 时默认调用 `openresty`。
* Agent 周期性健康检查会请求 `http://127.0.0.1:<openresty_observability_port>/openflare/stub_status`,不再通过高频 `openresty -t` 判断运行时健康;配置应用、启动恢复和 reload 前校验仍会执行 `openresty -t -c <main_config_path>`。
* Agent 会初始化并定期更新 `mmdb_path`,供 OpenResty WAF Lua 执行国家级地域规则;更新失败只记录警告,不阻断同步或 reload。
* 当激活配置引用 Pages 部署时,Agent 会在应用 OpenResty 配置前,将部署包下载、校验并解压到 `pages_dir`,OpenResty 通过该目录服务静态文件。
* 如果 `agent.json` 不存在,但 `OPENFLARE_SERVER_URL` 与 Token 等环境变量足够,Agent 可以直接启动;两者同时存在时环境变量优先。
* Agent 未配置 `node_ip` 时,会优先通过 `https://realip.cc` 获取真实出口公网 IP,适配 Docker/NAT 场景;该请求失败时,才退回本机网卡探测并优先选择公网 IPv4。
* Agent 自动探测到私网 `node_ip` 时,Server 会在注册/心跳阶段优先保留 Agent 直连来源的公网地址,避免 NAT/多网卡场景误登记内网网卡地址。
* 在管理端开启“锁定节点 IP”后,Server 会保留管理端填写的节点 IP,后续 Agent 注册、HTTP 心跳或 WebSocket 状态上报不会覆盖该字段;关闭锁定后,下一次上报可重新回填。
## Relay 环境变量
| 环境变量 | 作用 | 默认值 |
| --- | --- | --- |
| `LOG_LEVEL` | Relay 日志等级 | `info` |
| `OPENFLARE_SERVER_URL` | 控制面地址,可覆盖 `relay.json` | 空 |
| `OPENFLARE_AGENT_TOKEN` | 节点专属认证 Token,可覆盖 `relay.json` | 空 |
| `OPENFLARE_DISCOVERY_TOKEN` | 首次自动注册 Token,可覆盖 `relay.json` | 空 |
| `OPENFLARE_NODE_NAME` | 节点名称,可覆盖 `relay.json` | 空 |
| `OPENFLARE_NODE_IP` | 节点 IP,可覆盖 `relay.json` | 空 |
| `OPENFLARE_DATA_DIR` | Relay 数据目录,可覆盖 `relay.json` | 空 |
| `OPENFLARE_FRPS_PATH` | frps 二进制路径,可覆盖 `relay.json` | 空 |
## Relay 命令行参数
| 参数 | 作用 | 默认值 |
| --- | --- | --- |
| `-config` | 指定 Relay 配置文件路径 | `./relay.json` |
## Relay 配置字段
| 字段 | 作用 | 是否必填 | 默认值/行为 |
| --- | --- | --- | --- |
| `server_url` | 控制面地址 | 是 | 无 |
| `agent_token` | 中继节点专属认证 Token | 与 `discovery_token` 二选一 | 空 |
| `discovery_token` | 首次自动注册使用的全局 Token | 与 `agent_token` 二选一 | 空 |
| `node_name` | 节点名称 | 否 | 自动使用主机名 |
| `node_ip` | 中继节点 IP,用于接收穿透流量 | 否 | 自动探测,优先使用公网出口 IP;失败时退回网卡探测 |
| `frps_path` | frps 二进制路径 | 否 | `frps`(在系统 PATH 中寻找) |
| `data_dir` | Relay 运行时数据目录 | 否 | 配置文件所在目录下的 `data` |
| `state_path` | Relay 本地状态文件存储路径 | 否 | `data_dir/relay-state.json` |
| `heartbeat_interval` | 心跳间隔 | 否 | `10000` 毫秒,支持 Go duration 字符串 |
| `request_timeout` | HTTP 请求超时 | 否 | `10000` 毫秒,支持 Go duration 字符串 |
## OpenFlared (Client) 环境变量
| 环境变量 | 作用 | 默认值 |
| --- | --- | --- |
| `LOG_LEVEL` | Client 日志等级 | `info` |
| `OPENFLARE_SERVER_URL` | 控制面地址,可覆盖 `flared.json` | 空 |
| `OPENFLARE_TUNNEL_TOKEN` | 隧道专属认证 Token,可覆盖 `flared.json` | 空 |
| `OPENFLARE_DATA_DIR` | Client 数据目录,可覆盖 `flared.json` | 空 |
| `OPENFLARE_FRPC_PATH` | frpc 二进制路径,可覆盖 `flared.json` | 空 |
## OpenFlared (Client) 命令行参数
| 参数 | 作用 | 默认值 |
| --- | --- | --- |
| `-config` | 指定 Client 配置文件路径 | `./flared.json` |
## OpenFlared (Client) 配置字段
| 字段 | 作用 | 是否必填 | 默认值/行为 |
| --- | --- | --- | --- |
| `server_url` | 控制面地址 | 是 | 无 |
| `tunnel_token` | 隧道专属认证 Token | 是 | 无 |
| `frpc_path` | frpc 二进制路径 | 否 | `frpc`(在系统 PATH 中寻找) |
| `data_dir` | Client 运行时数据目录 | 否 | 配置文件所在目录下的 `data` |
| `state_path` | Client 本地状态文件存储路径 | 否 | `data_dir/flared-state.json` |
| `heartbeat_interval` | 心跳间隔 | 否 | `10000` 毫秒,支持 Go duration 字符串 |
| `sync_interval` | 配置拉取同步间隔 | 否 | `30000` 毫秒,支持 Go duration 字符串 |
| `request_timeout` | HTTP 请求超时 | 否 | `10000` 毫秒,支持 Go duration 字符串 |
## 常见配置组合
### 生产 Server + PostgreSQL
```bash
export SESSION_SECRET='replace-with-a-long-random-string'
export JWT_SECRET='replace-with-a-long-random-string'
export DSN='postgres://openflare:replace-with-strong-password@postgres:5432/openflare?sslmode=disable'
export GIN_MODE='release'
export LOG_LEVEL='info'
@@ -210,7 +304,7 @@ export LOG_LEVEL='info'
### 本地 Server + SQLite
```bash
export SESSION_SECRET='dev-session-secret'
export JWT_SECRET='dev-jwt-secret'
export SQLITE_PATH='./openflare-dev.db'
export LOG_LEVEL='debug'
go run .
@@ -243,17 +337,51 @@ go run .
"cert_dir": "/var/lib/openflare-agent/etc/nginx/certs",
"lua_dir": "/var/lib/openflare-agent/etc/nginx/lua",
"runtime_config_dir": "/var/lib/openflare-agent/etc/openflare",
"pages_dir": "/var/lib/openflare-agent/var/lib/openflare/pages",
"heartbeat_interval": 10000,
"request_timeout": 10000
}
```
### Relay (中继端) 默认配置
`relay.json`:
```json
{
"server_url": "http://your-server:3000",
"agent_token": "replace-with-relay-auth-token",
"frps_path": "frps",
"data_dir": "/opt/openflare-relay/data",
"heartbeat_interval": 10000,
"request_timeout": 10000
}
```
### OpenFlared (内网客户端) 默认配置
`flared.json`:
```json
{
"server_url": "http://your-server:3000",
"tunnel_token": "replace-with-tunnel-token",
"frpc_path": "frpc",
"data_dir": "/opt/openflared/data",
"heartbeat_interval": 10000,
"sync_interval": 30000,
"request_timeout": 10000
}
```
## 维护要求
以下内容变化时,必须同步更新本文档:
* Server 命令行参数。
* Server 环境变量。
* Agent 命令行参数。
* Agent 配置字段。
* Agent 命令行参数与配置字段。
* Relay 命令行参数与配置字段。
* Client 命令行参数与配置字段。
* 任一配置项的默认值、用途或示例。
+2 -1
View File
@@ -9,4 +9,5 @@
| [配置项](./configuration.md) | Server 环境变量、命令行参数、运行时 Option 与 Agent 配置字段 |
| [命令与脚本](./cli.md) | 常用启动、构建、测试、安装和卸载命令 |
| [API 约定](./api.md) | 管理端 API 与 Agent API 的响应结构、鉴权和路径约定 |
| [仓库结构](../design/repository.md) | `openflare_server`、`openflare_agent`、`openflare_server/web` 与 `docs` 的职责 |
| [仓库结构](../design/repository.md) | `openflare_server`、`openflare_agent`、`openflare_relay`、`openflared` 模块的职责与分层目录说明 |
| [部署与升级](../deployment/) | Server 与 Agent 的部署、配置与升级指南(见专属分区) |
-106
View File
@@ -1,106 +0,0 @@
# 启动 Server
你会学到:如何从源码构建管理端前端、启动 OpenFlare Server、选择 SQLite 或 PostgreSQL,并访问 Swagger。
OpenFlare Server 是 Gin + GORM 单体控制面,负责管理端 UI、管理 API、Agent API、配置渲染、版本发布、数据存储与聚合查询。
## 前置条件
| 项目 | 要求 |
| --- | --- |
| Go | `1.25+` |
| Node.js | `18+` |
| pnpm | 推荐通过 `corepack enable` 使用项目声明的 pnpm |
| 数据库 | SQLite 文件目录可写,或可访问的 PostgreSQL 实例 |
生产环境建议显式配置 `SESSION_SECRET`,并优先使用 PostgreSQL。
## 构建管理端前端
Go Server 会托管 `openflare_server/web/build` 中的静态产物。源码启动前先构建前端:
```bash
cd openflare_server/web
corepack enable
pnpm install
pnpm build
```
常用前端检查:
```bash
pnpm lint
pnpm typecheck
pnpm test
```
## 使用 SQLite 启动
```bash
cd openflare_server
export SESSION_SECRET='replace-with-a-long-random-string'
export SQLITE_PATH='./openflare.db'
export LOG_LEVEL='info'
go run .
```
默认监听 `3000` 端口,访问:
```text
http://localhost:3000
```
## 使用 PostgreSQL 启动
```bash
cd openflare_server
export SESSION_SECRET='replace-with-a-long-random-string'
export DSN='postgres://openflare:secret@127.0.0.1:5432/openflare?sslmode=disable'
export LOG_LEVEL='info'
go run .
```
`DSN` 设置后优先于 SQLite。`DSN` 与兼容旧命名的 `SQL_DSN` 同时存在时,优先使用 `DSN`。
如果目标 PostgreSQL 数据库为空且本地 `SQLITE_PATH` 文件存在,Server 启动阶段会尝试把 SQLite 数据迁移到 PostgreSQL,并在日志中输出迁移进度。
## 命令行参数
```bash
go run . --port 3000 --log-dir ./logs
```
| 参数 | 作用 | 默认值 |
| --- | --- | --- |
| `--port` | 指定 Server 监听端口 | `3000` |
| `--log-dir` | 指定日志目录 | 空,输出到标准输出 |
| `--version` | 输出版本后退出 | `false` |
| `--help` | 输出帮助后退出 | `false` |
## 首次登录
默认账号:
| 用户名 | 密码 |
| --- | --- |
| `root` | `123456` |
首次登录后请立即修改默认密码。
## Swagger
登录管理端后访问:
```text
http://localhost:3000/swagger/index.html
```
本地重新生成 Swagger:
```bash
go install github.com/swaggo/swag/cmd/swag@v1.16.4
cd openflare_server
swag init -g main.go -o docs
```
Swagger 生成文件位于 `openflare_server/docs`。
+12 -2
View File
@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1.7
ARG VERSION=dev
FROM golang:1.25-alpine AS builder
@@ -10,12 +11,20 @@ ENV CGO_ENABLED=0 \
GOOS=${TARGETOS} \
GOARCH=${TARGETARCH}
WORKDIR /build
COPY openflare_server/go.mod openflare_server/go.sum ./openflare_server/
COPY openflare_agent/go.mod openflare_agent/go.sum ./openflare_agent/
WORKDIR /build/openflare_agent
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
WORKDIR /build
COPY openflare_server ./openflare_server
COPY openflare_agent ./openflare_agent
WORKDIR /build/openflare_agent
RUN go mod download
RUN go build -trimpath -ldflags "-s -w -X 'openflare-agent/internal/config.Version=$VERSION'" -o /build/openflare-agent ./cmd/agent
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -trimpath -ldflags "-s -w -X 'openflare-agent/internal/config.Version=$VERSION'" -o /build/openflare-agent ./cmd/agent
FROM openresty/openresty:alpine
@@ -32,3 +41,4 @@ COPY --from=builder /build/openflare-agent /usr/local/bin/openflare-agent
EXPOSE 80 443 18081
ENTRYPOINT ["/usr/local/bin/openflare-agent"]
CMD ["-config", "/etc/openflare/agent.json"]
+4 -1
View File
@@ -71,6 +71,7 @@ func main() {
LuaDir: cfg.LuaDir,
NginxLuaDir: cfg.OpenrestyLuaDir,
RuntimeConfigDir: cfg.RuntimeConfigDir,
PagesDir: cfg.PagesDir,
OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyObservabilityPort),
OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort,
OpenrestyResolverDirective: "",
@@ -89,12 +90,14 @@ func main() {
slog.Error("ensure managed lua assets failed", "error", err)
os.Exit(1)
}
syncService := syncservice.New(client, runtimeManager, stateStore)
syncService.SetPagesDir(cfg.PagesDir)
runner := &agent.Runner{
Config: cfg,
StateStore: stateStore,
ObservabilityBuffer: observabilityBuffer,
HeartbeatService: heartbeat.New(client),
SyncService: syncservice.New(client, runtimeManager, stateStore),
SyncService: syncService,
Updater: updater.New(),
RuntimeManager: runtimeManager,
WebSocketService: wsClient,
+3 -5
View File
@@ -1,17 +1,15 @@
module openflare-agent
go 1.25.0
go 1.25.7
require (
golang.org/x/net v0.53.0
openflare v0.0.0
)
require openflare v0.0.0
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
)
+55 -32
View File
@@ -12,6 +12,7 @@ import (
"openflare-agent/internal/observability"
"openflare-agent/internal/protocol"
"openflare-agent/internal/state"
"openflare-agent/internal/wsclient"
)
type HeartbeatService interface {
@@ -211,53 +212,75 @@ func (r *Runner) startWebSocket(ctx context.Context, nodeID string) (<-chan erro
return done, nil
}
type agentWSHandler struct {
runner *Runner
conn protocol.WebSocketConnection
nodeID string
statusTicker *time.Ticker
}
func (h *agentWSHandler) OnConnect(ctx context.Context) error {
return h.runner.sendWebSocketStatus(ctx, h.nodeID, h.conn)
}
func (h *agentWSHandler) HandleMessage(ctx context.Context, msg wsclient.WSMessage) error {
var payloadBytes []byte
if msg.Payload != nil {
payloadBytes = []byte(msg.Payload)
}
protoMsg := protocol.WSMessage{
Type: msg.Type,
Payload: payloadBytes,
}
changed, err := h.runner.handleWebSocketMessage(ctx, protoMsg, h.conn)
if err != nil {
return err
}
if changed {
h.statusTicker.Reset(h.runner.Config.HeartbeatInterval.Duration())
}
return nil
}
func (h *agentWSHandler) OnClose(err error) {
slog.Error("agent ws receive failed", "error", err)
}
func (r *Runner) runWebSocket(ctx context.Context, nodeID string, conn protocol.WebSocketConnection) error {
slog.Debug("agent ws connected", "url", conn.URL(), "node_id", nodeID)
statusTicker := time.NewTicker(r.Config.HeartbeatInterval.Duration())
defer statusTicker.Stop()
messages := make(chan protocol.WSMessage, 8)
readDone := make(chan error, 1)
childCtx, cancel := context.WithCancel(ctx)
defer cancel()
// Start status ticker sender in background
go func() {
for {
message, err := conn.Receive()
if err != nil {
readDone <- err
return
}
select {
case messages <- message:
case <-ctx.Done():
readDone <- ctx.Err()
case <-childCtx.Done():
return
case <-statusTicker.C:
if err := r.sendWebSocketStatus(childCtx, nodeID, conn); err != nil {
slog.Error("agent ws send status failed", "error", err)
_ = conn.Close()
return
}
}
}
}()
if err := r.sendWebSocketStatus(ctx, nodeID, conn); err != nil {
return err
wsConn, ok := conn.(*wsclient.Connection)
if !ok {
return errors.New("invalid websocket connection type")
}
for {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-readDone:
return err
case <-statusTicker.C:
if err := r.sendWebSocketStatus(ctx, nodeID, conn); err != nil {
return err
}
case message := <-messages:
changed, err := r.handleWebSocketMessage(ctx, message, conn)
if err != nil {
return err
}
if changed {
statusTicker.Reset(r.Config.HeartbeatInterval.Duration())
}
}
}
return wsConn.RunReceiveLoop(childCtx, &agentWSHandler{
runner: r,
conn: conn,
nodeID: nodeID,
statusTicker: statusTicker,
})
}
func (r *Runner) sendWebSocketStatus(ctx context.Context, nodeID string, conn protocol.WebSocketConnection) error {
+10
View File
@@ -23,6 +23,7 @@ const (
defaultCertDirRelativePath = "etc/nginx/certs"
defaultLuaDirRelativePath = "etc/nginx/lua"
defaultRuntimeConfigDirRelativePath = "etc/openflare"
defaultPagesDirRelativePath = "var/lib/openflare/pages"
defaultMMDBRelativePath = "etc/openflare/GeoLite2-Country.mmdb"
defaultAccessLogRelativePath = "var/log/openflare/access.log"
defaultStateRelativePath = "var/lib/openflare/agent-state.json"
@@ -57,6 +58,7 @@ type Config struct {
LuaDir string `json:"lua_dir"`
OpenrestyLuaDir string `json:"openresty_lua_dir"`
RuntimeConfigDir string `json:"runtime_config_dir"`
PagesDir string `json:"pages_dir"`
MMDBPath string `json:"mmdb_path"`
MMDBUpdateInterval MillisecondDuration `json:"mmdb_update_interval"`
MMDBDownloadURL string `json:"mmdb_download_url"`
@@ -86,6 +88,7 @@ type configFile struct {
LuaDir string `json:"lua_dir"`
OpenrestyLuaDir string `json:"openresty_lua_dir"`
RuntimeConfigDir string `json:"runtime_config_dir"`
PagesDir string `json:"pages_dir"`
MMDBPath string `json:"mmdb_path"`
MMDBUpdateInterval MillisecondDuration `json:"mmdb_update_interval"`
MMDBDownloadURL string `json:"mmdb_download_url"`
@@ -128,6 +131,7 @@ func Load(path string) (*Config, error) {
LuaDir: file.LuaDir,
OpenrestyLuaDir: file.OpenrestyLuaDir,
RuntimeConfigDir: file.RuntimeConfigDir,
PagesDir: file.PagesDir,
MMDBPath: file.MMDBPath,
MMDBUpdateInterval: file.MMDBUpdateInterval,
MMDBDownloadURL: file.MMDBDownloadURL,
@@ -190,6 +194,9 @@ func applyDefaults(cfg *Config, baseDir string) {
if cfg.RuntimeConfigDir == "" {
cfg.RuntimeConfigDir = joinManagedPath(cfg.DataDir, defaultRuntimeConfigDirRelativePath)
}
if cfg.PagesDir == "" {
cfg.PagesDir = joinManagedPath(cfg.DataDir, defaultPagesDirRelativePath)
}
if cfg.MMDBPath == "" {
cfg.MMDBPath = joinManagedPath(cfg.DataDir, defaultMMDBRelativePath)
}
@@ -231,6 +238,7 @@ func normalizeManagedPaths(cfg *Config) {
&cfg.LuaDir,
&cfg.OpenrestyLuaDir,
&cfg.RuntimeConfigDir,
&cfg.PagesDir,
&cfg.StatePath,
&cfg.ObservabilityBufferPath,
&cfg.MMDBPath,
@@ -251,6 +259,7 @@ func hasEnvConfig() bool {
"OPENFLARE_NODE_IP",
"OPENFLARE_DATA_DIR",
"OPENFLARE_OPENRESTY_PATH",
"OPENFLARE_PAGES_DIR",
"OPENFLARE_HEARTBEAT_INTERVAL",
"OPENFLARE_REQUEST_TIMEOUT",
"OPENFLARE_OPENRESTY_OBSERVABILITY_PORT",
@@ -281,6 +290,7 @@ func applyEnvOverrides(cfg *Config) {
overrideString("OPENFLARE_NODE_IP", &cfg.NodeIP)
overrideString("OPENFLARE_DATA_DIR", &cfg.DataDir)
overrideString("OPENFLARE_OPENRESTY_PATH", &cfg.OpenrestyPath)
overrideString("OPENFLARE_PAGES_DIR", &cfg.PagesDir)
overrideString("OPENFLARE_MMDB_PATH", &cfg.MMDBPath)
overrideString("OPENFLARE_MMDB_DOWNLOAD_URL", &cfg.MMDBDownloadURL)
if value := strings.TrimSpace(os.Getenv("OPENFLARE_HEARTBEAT_INTERVAL")); value != "" {
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
@@ -86,6 +87,23 @@ func (c *Client) SyncWAFIPGroups(ctx context.Context, payload protocol.WAFIPGrou
return &resp.Data, nil
}
func (c *Client) DownloadPagesDeploymentPackage(ctx context.Context, deploymentID uint) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+fmt.Sprintf("/api/agent/pages/deployments/%d/package", deploymentID), nil)
if err != nil {
return nil, err
}
req.Header.Set("X-Agent-Token", c.token)
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, errors.New(res.Status)
}
return io.ReadAll(res.Body)
}
func (c *Client) SetToken(token string) {
c.token = strings.TrimSpace(token)
slog.Debug("http client token updated")
+11
View File
@@ -141,6 +141,7 @@ type Manager struct {
LuaDir string
NginxLuaDir string
RuntimeConfigDir string
PagesDir string
OpenrestyObservabilityListen string
OpenrestyObservabilityPort int
OpenrestyResolverDirective string
@@ -422,6 +423,9 @@ func (m *Manager) CurrentChecksum() (string, error) {
normalizedRoute = strings.ReplaceAll(normalizedRoute, luaDir+"/pow/static", openrestyrender.PowStaticDirPlaceholder)
normalizedRoute = strings.ReplaceAll(normalizedRoute, luaDir, openrestyrender.LuaDirPlaceholder)
}
if pagesDir := m.pagesRuntimePath(); pagesDir != "" {
normalizedRoute = strings.ReplaceAll(normalizedRoute, pagesDir, openrestyrender.PagesDirPlaceholder)
}
files, err := m.readManagedSupportFiles()
if err != nil {
return "", err
@@ -1130,6 +1134,9 @@ func (m *Manager) renderRouteConfig(content string) string {
rendered = strings.ReplaceAll(rendered, openrestyrender.LuaDirPlaceholder, luaDir)
rendered = strings.ReplaceAll(rendered, openrestyrender.PowStaticDirPlaceholder, luaDir+"/pow/static")
}
if pagesDir := m.pagesRuntimePath(); pagesDir != "" {
rendered = strings.ReplaceAll(rendered, openrestyrender.PagesDirPlaceholder, pagesDir)
}
return rendered
}
@@ -1258,6 +1265,10 @@ func (m *Manager) luaRuntimePath() string {
return filepath.ToSlash(m.NginxLuaDir)
}
func (m *Manager) pagesRuntimePath() string {
return filepath.ToSlash(strings.TrimSpace(m.PagesDir))
}
func checksum(content string) string {
sum := sha256.Sum256([]byte(content))
return hex.EncodeToString(sum[:])
@@ -549,7 +549,7 @@ func TestManagerEnsureLuaAssetsWritesReadableFiles(t *testing.T) {
if err != nil {
t.Fatalf("failed to read pow lua file: %v", err)
}
if !strings.Contains(string(data), filepath.ToSlash(manager.RuntimeConfigDir)+"/pow_config.json") {
if !strings.Contains(string(data), filepath.ToSlash(manager.RuntimeConfigDir)+"/waf_config.json") {
t.Fatalf("expected pow lua to read runtime config dir, got %s", string(data))
}
}
+75 -23
View File
@@ -39,9 +39,9 @@ end
-- Lazy-load pow_config from file; reload when content changes
local function load_pow_config()
local config_paths = {
"__OPENFLARE_RUNTIME_CONFIG_DIR__/pow_config.json",
"/etc/nginx/openflare-lua/pow_config.json",
"/usr/local/openresty/nginx/conf/pow_config.json"
"__OPENFLARE_RUNTIME_CONFIG_DIR__/waf_config.json",
"/etc/nginx/openflare-lua/waf_config.json",
"/usr/local/openresty/nginx/conf/waf_config.json"
}
for _, config_path in ipairs(config_paths) do
local f = io.open(config_path, "r")
@@ -54,7 +54,7 @@ local function load_pow_config()
return
end
-- Clear old domain entries
-- Clear old domain/site entries
local old_keys = pow_config_dict:get("_domain_keys")
if old_keys then
for domain in string.gmatch(old_keys, "[^\n]+") do
@@ -64,15 +64,38 @@ local function load_pow_config()
local domain_keys = {}
if content and content ~= "" and content ~= "{}" then
local ok, entries = pcall(cjson.decode, content)
if ok and entries and type(entries) == "table" then
for _, entry in ipairs(entries) do
if entry.domains then
for _, domain in ipairs(entry.domains) do
pow_config_dict:set(domain, cjson.encode(entry), 0)
domain_keys[#domain_keys+1] = domain
local ok, decoded = pcall(cjson.decode, content)
if ok and decoded and decoded.rule_groups and decoded.site_rule_groups then
-- Build rule groups map (group ID -> PoWConfig)
local groups = {}
for _, group in ipairs(decoded.rule_groups) do
if group.pow_enabled then
groups[tostring(group.id)] = group.pow_config
end
end
-- Build site name to pow_config map
for site, group_ids in pairs(decoded.site_rule_groups) do
local pow_config = nil
-- Check custom group IDs first
for _, id in ipairs(group_ids) do
pow_config = groups[tostring(id)]
if pow_config then
break
end
end
-- If not found, check global group IDs
if not pow_config then
for _, group in ipairs(decoded.rule_groups) do
if group.is_global and group.pow_enabled then
pow_config = group.pow_config
break
end
end
end
if pow_config then
pow_config_dict:set(site, cjson.encode({enabled = true, config = pow_config}), 0)
domain_keys[#domain_keys+1] = site
end
end
end
end
@@ -91,7 +114,12 @@ if not host or host == "" then
return
end
local config_raw = pow_config_dict:get(host)
local site = ngx.var.openflare_waf_site or ""
if site == "" then
site = host
end
local config_raw = pow_config_dict:get(site)
if not config_raw then
return
end
@@ -199,17 +227,22 @@ local args = ngx.req.get_uri_args()
local host = args["host"] or ngx.var.host or ""
local redir = args["redir"] or ""
local config_raw = pow_config_dict:get(host)
local site = ngx.var.openflare_waf_site or ""
if site == "" then
site = host
end
local config_raw = pow_config_dict:get(site)
if not config_raw then
ngx.status = 403
ngx.say("PoW not configured for this host")
ngx.say("PoW not configured for this site")
return
end
local ok, route_config = pcall(cjson.decode, config_raw)
if not ok or not route_config or not route_config.enabled then
ngx.status = 403
ngx.say("PoW not enabled for this host")
ngx.say("PoW not enabled for this site")
return
end
@@ -236,8 +269,27 @@ local challenge_info = cjson.encode({
pow_challenges:set(challenge_id, challenge_info, challenge_ttl)
local static_prefix = "/.within.website/x/cmd/anubis/static/"
local title = "Making sure you're not a bot!"
local accept_lang = ngx.var.http_accept_language or ""
local lang = "en"
if string.find(accept_lang, "zh") then
lang = "zh-CN"
end
local t_title = "Making sure you're not a bot!"
local t_status = "Loading..."
local t_protected = "This site is protected by a Proof-of-Work challenge. Your browser will solve a small puzzle before the upstream response is shown."
local t_why = "Why am I seeing this?"
local t_why_desc = "OpenFlare is asking your browser to complete a lightweight computation to distinguish normal browser traffic from automated abuse. This should finish automatically."
local t_noscript = "JavaScript is required to pass this verification. Please enable JavaScript and reload."
if lang == "zh-CN" then
t_title = "正在确认你是不是机器人!"
t_status = "加载中..."
t_protected = "本网站受工作量证明(Proof-of-Work)挑战保护。在显示源站响应之前,您的浏览器将解决一个微型谜题。"
t_why = "为什么我会看到这个?"
t_why_desc = "OpenFlare 正在要求您的浏览器完成一项轻量级计算,以区分正常的浏览器流量和自动化的恶意请求。这应该会自动完成。"
t_noscript = "很遗憾,您必须启用 JavaScript 才能通过这项验证。请开启 JavaScript 并刷新页面。"
end
ngx.header.content_type = "text/html; charset=utf-8"
ngx.say([[<!DOCTYPE html>
@@ -246,7 +298,7 @@ ngx.say([[<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>]] .. title .. [[</title>
<title>]] .. t_title .. [[</title>
<link rel="stylesheet" href="]] .. static_prefix .. [[css/xess.css">
<style>
body,html{height:100%;display:flex;justify-content:center;align-items:center;margin-left:auto;margin-right:auto}
@@ -272,17 +324,17 @@ body,html{height:100%;display:flex;justify-content:center;align-items:center;mar
</head>
<body id="top">
<main>
<h1 id="title" class="centered-div">]] .. title .. [[</h1>
<h1 id="title" class="centered-div">]] .. t_title .. [[</h1>
<div class="centered-div">
<img id="image" style="width:100%;max-width:256px;" src="]] .. static_prefix .. [[img/pensive.webp?cacheBuster=openflare-pow">
<p id="status">Loading...</p>
<p>This site is protected by a Proof-of-Work challenge. Your browser will solve a small puzzle before the upstream response is shown.</p>
<p id="status">]] .. t_status .. [[</p>
<p>]] .. t_protected .. [[</p>
<div id="progress" role="progressbar" aria-labelledby="status"><div class="bar-inner"></div></div>
<details>
<summary>Why am I seeing this?</summary>
<p>OpenFlare is asking your browser to complete a lightweight computation to distinguish normal browser traffic from automated abuse. This should finish automatically.</p>
<summary>]] .. t_why .. [[</summary>
<p>]] .. t_why_desc .. [[</p>
</details>
<noscript><p>JavaScript is required to pass this verification. Please enable JavaScript and reload.</p></noscript>
<noscript><p>]] .. t_noscript .. [[</p></noscript>
</div>
</main>
<script type="module" src="]] .. static_prefix .. [[js/main.mjs"></script>
+328
View File
@@ -0,0 +1,328 @@
package sync
import (
"archive/zip"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"openflare-agent/internal/protocol"
)
type pagesSourceDocument struct {
Routes []pagesSourceRoute `json:"routes"`
}
type pagesSourceRoute struct {
UpstreamType string `json:"upstream_type"`
PagesDeployment *pagesDeploymentSource `json:"pages_deployment"`
}
type pagesDeploymentSource struct {
DeploymentID uint `json:"deployment_id"`
Checksum string `json:"checksum"`
}
type pagesDeploymentMarker struct {
DeploymentID uint `json:"deployment_id"`
Checksum string `json:"checksum"`
}
func (s *Service) syncPagesDeployments(ctx context.Context, config *protocol.ActiveConfigResponse) error {
deployments, err := referencedPagesDeployments(config)
if err != nil {
return err
}
if len(deployments) == 0 {
return nil
}
if strings.TrimSpace(s.pagesDir) == "" {
return errors.New("pages_dir is required when active config references Pages deployments")
}
for _, deployment := range deployments {
if err := s.ensurePagesDeployment(ctx, deployment); err != nil {
return err
}
}
return nil
}
func (s *Service) ensurePagesDeployment(ctx context.Context, deployment pagesDeploymentSource) error {
currentDir := pagesCurrentDir(s.pagesDir, deployment.DeploymentID)
if markerMatches(currentDir, deployment) {
return nil
}
packageBytes, err := s.client.DownloadPagesDeploymentPackage(ctx, deployment.DeploymentID)
if err != nil {
return fmt.Errorf("download Pages deployment %d: %w", deployment.DeploymentID, err)
}
if got := checksumBytes(packageBytes); got != deployment.Checksum {
return fmt.Errorf("Pages deployment %d checksum mismatch: expected %s, got %s", deployment.DeploymentID, deployment.Checksum, got)
}
releaseDir := pagesReleaseDir(s.pagesDir, deployment.DeploymentID, deployment.Checksum)
if !markerMatches(releaseDir, deployment) {
if err := extractPagesPackage(packageBytes, releaseDir, deployment); err != nil {
return err
}
}
return switchPagesCurrentDir(s.pagesDir, deployment.DeploymentID, releaseDir)
}
func referencedPagesDeployments(config *protocol.ActiveConfigResponse) ([]pagesDeploymentSource, error) {
if config == nil || strings.TrimSpace(config.SourceConfigJSON) == "" {
return nil, nil
}
var doc pagesSourceDocument
if err := json.Unmarshal([]byte(config.SourceConfigJSON), &doc); err != nil {
return nil, fmt.Errorf("decode Pages references: %w", err)
}
seen := make(map[uint]struct{})
result := make([]pagesDeploymentSource, 0)
for _, route := range doc.Routes {
if strings.ToLower(strings.TrimSpace(route.UpstreamType)) != "pages" || route.PagesDeployment == nil {
continue
}
deploymentID := route.PagesDeployment.DeploymentID
checksum := strings.TrimSpace(route.PagesDeployment.Checksum)
if deploymentID == 0 || checksum == "" {
return nil, errors.New("Pages deployment snapshot is incomplete")
}
if _, ok := seen[deploymentID]; ok {
continue
}
seen[deploymentID] = struct{}{}
result = append(result, pagesDeploymentSource{DeploymentID: deploymentID, Checksum: checksum})
}
return result, nil
}
func findCommonRootPrefix(files []*zip.File) (string, error) {
var firstFilePath string
hasMultipleFiles := false
for _, item := range files {
relativePath, skip, err := normalizePagesArchivePath(item.Name)
if err != nil {
return "", err
}
if skip {
continue
}
normalizedPath := filepath.ToSlash(relativePath)
if firstFilePath == "" {
firstFilePath = normalizedPath
} else {
hasMultipleFiles = true
}
}
if firstFilePath == "" {
return "", nil
}
parts := strings.Split(firstFilePath, "/")
if len(parts) <= 1 {
return "", nil
}
commonPrefix := parts[0] + "/"
if hasMultipleFiles {
for _, item := range files {
relativePath, skip, err := normalizePagesArchivePath(item.Name)
if err != nil {
return "", err
}
if skip {
continue
}
normalizedPath := filepath.ToSlash(relativePath)
if !strings.HasPrefix(normalizedPath, commonPrefix) {
return "", nil
}
}
}
return commonPrefix, nil
}
func extractPagesPackage(packageBytes []byte, releaseDir string, deployment pagesDeploymentSource) error {
tmpDir := releaseDir + ".tmp"
_ = os.RemoveAll(tmpDir)
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return err
}
reader, err := zip.NewReader(bytes.NewReader(packageBytes), int64(len(packageBytes)))
if err != nil {
_ = os.RemoveAll(tmpDir)
return fmt.Errorf("open Pages zip: %w", err)
}
commonPrefix, err := findCommonRootPrefix(reader.File)
if err != nil {
_ = os.RemoveAll(tmpDir)
return err
}
for _, item := range reader.File {
relativePath, skip, err := normalizePagesArchivePath(item.Name)
if err != nil {
_ = os.RemoveAll(tmpDir)
return err
}
if skip {
continue
}
if commonPrefix != "" {
slashPath := filepath.ToSlash(relativePath)
if strings.HasPrefix(slashPath, commonPrefix) {
relativePath = filepath.FromSlash(strings.TrimPrefix(slashPath, commonPrefix))
}
}
if item.FileInfo().Mode()&os.ModeSymlink != 0 {
_ = os.RemoveAll(tmpDir)
return fmt.Errorf("Pages package contains unsupported symlink: %s", relativePath)
}
if err := extractPagesFile(item, filepath.Join(tmpDir, relativePath)); err != nil {
_ = os.RemoveAll(tmpDir)
return err
}
}
if err := writePagesMarker(tmpDir, deployment); err != nil {
_ = os.RemoveAll(tmpDir)
return err
}
_ = os.RemoveAll(releaseDir)
return os.Rename(tmpDir, releaseDir)
}
func extractPagesFile(item *zip.File, targetPath string) error {
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
return err
}
source, err := item.Open()
if err != nil {
return err
}
defer source.Close()
target, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, item.FileInfo().Mode().Perm())
if err != nil {
return err
}
defer target.Close()
_, err = io.Copy(target, source)
return err
}
func switchPagesCurrentDir(baseDir string, deploymentID uint, releaseDir string) error {
currentDir := pagesCurrentDir(baseDir, deploymentID)
previousDir := currentDir + ".previous"
_ = os.RemoveAll(previousDir)
if err := os.MkdirAll(filepath.Dir(currentDir), 0o755); err != nil {
return err
}
if _, err := os.Stat(currentDir); err == nil {
if err := os.Rename(currentDir, previousDir); err != nil {
return err
}
}
if err := copyPagesDir(releaseDir, currentDir); err != nil {
_ = os.RemoveAll(currentDir)
if _, restoreErr := os.Stat(previousDir); restoreErr == nil {
_ = os.Rename(previousDir, currentDir)
}
return err
}
_ = os.RemoveAll(previousDir)
return nil
}
func copyPagesDir(sourceDir string, targetDir string) error {
return filepath.WalkDir(sourceDir, func(sourcePath string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
relativePath, err := filepath.Rel(sourceDir, sourcePath)
if err != nil || relativePath == "." {
return err
}
targetPath := filepath.Join(targetDir, relativePath)
if entry.IsDir() {
return os.MkdirAll(targetPath, 0o755)
}
info, err := entry.Info()
if err != nil {
return err
}
input, err := os.Open(sourcePath)
if err != nil {
return err
}
defer input.Close()
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
return err
}
output, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode().Perm())
if err != nil {
return err
}
defer output.Close()
_, err = io.Copy(output, input)
return err
})
}
func normalizePagesArchivePath(raw string) (string, bool, error) {
name := strings.TrimSpace(filepath.ToSlash(raw))
if name == "" || strings.HasSuffix(name, "/") {
return "", true, nil
}
if strings.HasPrefix(name, "/") {
return "", false, fmt.Errorf("Pages package contains absolute path: %s", raw)
}
cleaned := path.Clean(name)
if cleaned == "." {
return "", true, nil
}
if cleaned == ".." || strings.HasPrefix(cleaned, "../") || strings.Contains(cleaned, "/../") {
return "", false, fmt.Errorf("Pages package path escapes deployment root: %s", raw)
}
return filepath.FromSlash(cleaned), false, nil
}
func markerMatches(dir string, deployment pagesDeploymentSource) bool {
data, err := os.ReadFile(filepath.Join(dir, ".openflare-pages.json"))
if err != nil {
return false
}
var marker pagesDeploymentMarker
if err := json.Unmarshal(data, &marker); err != nil {
return false
}
return marker.DeploymentID == deployment.DeploymentID && marker.Checksum == deployment.Checksum
}
func writePagesMarker(dir string, deployment pagesDeploymentSource) error {
data, err := json.Marshal(pagesDeploymentMarker{
DeploymentID: deployment.DeploymentID,
Checksum: deployment.Checksum,
})
if err != nil {
return err
}
return os.WriteFile(filepath.Join(dir, ".openflare-pages.json"), data, 0o644)
}
func pagesCurrentDir(baseDir string, deploymentID uint) string {
return filepath.Join(baseDir, "deployments", fmt.Sprintf("%d", deploymentID), "current")
}
func pagesReleaseDir(baseDir string, deploymentID uint, checksum string) string {
return filepath.Join(baseDir, "deployments", fmt.Sprintf("%d", deploymentID), "releases", checksum)
}
func checksumBytes(data []byte) string {
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
+9
View File
@@ -25,6 +25,7 @@ const (
type ConfigClient interface {
GetActiveConfig(ctx context.Context) (*protocol.ActiveConfigResponse, error)
DownloadPagesDeploymentPackage(ctx context.Context, deploymentID uint) ([]byte, error)
ReportApplyLog(ctx context.Context, payload protocol.ApplyLogPayload) error
SyncWAFIPGroups(ctx context.Context, payload protocol.WAFIPGroupSyncRequest) (*protocol.WAFIPGroupSyncResponse, error)
}
@@ -42,6 +43,11 @@ type Service struct {
client ConfigClient
nginxManager NginxManager
stateStore *state.Store
pagesDir string
}
func (s *Service) SetPagesDir(path string) {
s.pagesDir = strings.TrimSpace(path)
}
func New(client ConfigClient, nginxManager NginxManager, stateStore *state.Store) *Service {
@@ -225,6 +231,9 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool,
if err != nil {
return err
}
if err := s.syncPagesDeployments(ctx, config); err != nil {
return err
}
mainConfigChecksum := checksumString(rendered.mainConfig)
routeConfigChecksum := checksumString(rendered.routeConfig)
slog.Info("applying new openresty config", "mode", mode, "from_version", snapshot.CurrentVersion, "to_version", config.Version, "old_checksum", currentChecksum, "new_checksum", config.Checksum)
+165 -5
View File
@@ -1,7 +1,11 @@
package sync
import (
"archive/zip"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
@@ -19,10 +23,15 @@ type fakeExecutor struct {
reloadErr error
}
func testPagesSourceConfigJSON(deploymentID uint, checksum string) string {
return fmt.Sprintf(`{"routes":[{"id":1,"site_name":"pages","domain":"pages.example.com","domains":["pages.example.com"],"origin_url":"openflare-pages://project/1","upstreams":["openflare-pages://project/1"],"enabled":true,"upstream_type":"pages","pages_deployment":{"project_id":1,"project_slug":"pages","deployment_id":%d,"deployment_number":1,"checksum":"%s","entry_file":"index.html","spa_fallback_enabled":true,"local_root":"__OPENFLARE_PAGES_DIR__/deployments/%d/current"}}],"openresty_config":{"worker_processes":"auto","worker_connections":1024,"worker_rlimit_nofile":65535,"events_multi_accept_enabled":true,"keepalive_timeout":20,"keepalive_requests":1000,"client_header_timeout":15,"client_body_timeout":15,"client_max_body_size":"64m","large_client_header_buffers":"4 16k","send_timeout":30,"proxy_connect_timeout":3,"proxy_send_timeout":60,"proxy_read_timeout":60,"websocket_enabled":true,"proxy_request_buffering":false,"proxy_buffering_enabled":true,"proxy_buffers":"16 16k","proxy_buffer_size":"8k","proxy_busy_buffers_size":"64k","gzip_enabled":true,"gzip_min_length":1024,"gzip_comp_level":5,"cache_enabled":false,"cache_levels":"1:2","cache_inactive":"30m","cache_max_size":"1g","cache_key_template":"$scheme$host$request_uri","cache_lock_enabled":true,"cache_lock_timeout":"5s","cache_use_stale":"error timeout updating http_500 http_502 http_503 http_504","main_config_template":"worker_processes {{OpenRestyWorkerProcesses}};"},"waf":{"rule_groups":[],"bindings":[]}}`, deploymentID, checksum, deploymentID)
}
type fakeClient struct {
config protocol.ActiveConfigResponse
reports []protocol.ApplyLogPayload
fetchCalls int
config protocol.ActiveConfigResponse
reports []protocol.ApplyLogPayload
pagesPackages map[uint][]byte
fetchCalls int
}
type fakeManager struct {
@@ -67,6 +76,13 @@ func (f *fakeClient) GetActiveConfig(ctx context.Context) (*protocol.ActiveConfi
return &f.config, nil
}
func (f *fakeClient) DownloadPagesDeploymentPackage(ctx context.Context, deploymentID uint) ([]byte, error) {
if f.pagesPackages == nil {
return nil, fmt.Errorf("missing Pages package %d", deploymentID)
}
return f.pagesPackages[deploymentID], nil
}
func (f *fakeClient) ReportApplyLog(ctx context.Context, payload protocol.ApplyLogPayload) error {
f.reports = append(f.reports, payload)
return nil
@@ -174,11 +190,82 @@ func TestSyncOnceSuccess(t *testing.T) {
if client.reports[0].MainConfigChecksum == "" || client.reports[0].RouteConfigChecksum == "" {
t.Fatal("expected main and route config checksums to be reported")
}
if client.reports[0].SupportFileCount != 4 {
if client.reports[0].SupportFileCount != 3 {
t.Fatalf("expected support file count to be reported, got %d", client.reports[0].SupportFileCount)
}
}
func TestSyncOnceDownloadsPagesDeploymentBeforeApply(t *testing.T) {
packageBytes := testPagesPackage(t, map[string]string{"index.html": "hello"})
checksum := testBytesChecksum(packageBytes)
client := &fakeClient{
config: protocol.ActiveConfigResponse{
Version: "20260309-101",
Checksum: "pages-config-checksum",
SourceConfigJSON: testPagesSourceConfigJSON(7, checksum),
CreatedAt: time.Now().Format(time.RFC3339),
},
pagesPackages: map[uint][]byte{7: packageBytes},
}
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
nodeID, err := stateStore.EnsureNodeID()
if err != nil {
t.Fatalf("EnsureNodeID failed: %v", err)
}
snapshot, _ := stateStore.Load()
snapshot.NodeID = nodeID
if err = stateStore.Save(snapshot); err != nil {
t.Fatalf("save state failed: %v", err)
}
manager := &fakeManager{currentChecksum: "old-checksum"}
service := New(client, manager, stateStore)
pagesDir := t.TempDir()
service.SetPagesDir(pagesDir)
if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{Version: "20260309-101", Checksum: "pages-config-checksum"}); err != nil {
t.Fatalf("SyncOnce failed: %v", err)
}
data, err := os.ReadFile(filepath.Join(pagesDir, "deployments", "7", "current", "index.html"))
if err != nil {
t.Fatalf("expected Pages file to be extracted: %v", err)
}
if string(data) != "hello" {
t.Fatalf("unexpected Pages file content: %s", string(data))
}
if len(manager.applyRouteContents) != 1 || !strings.Contains(manager.applyRouteContents[0], "__OPENFLARE_PAGES_DIR__/deployments/7/current") {
t.Fatalf("expected Pages placeholder in rendered route config, got %#v", manager.applyRouteContents)
}
}
func TestSyncOnceRejectsPagesZipSlipBeforeApply(t *testing.T) {
packageBytes := testPagesPackage(t, map[string]string{"../escape.html": "bad", "index.html": "ok"})
checksum := testBytesChecksum(packageBytes)
client := &fakeClient{
config: protocol.ActiveConfigResponse{
Version: "20260309-102",
Checksum: "pages-config-checksum",
SourceConfigJSON: testPagesSourceConfigJSON(8, checksum),
CreatedAt: time.Now().Format(time.RFC3339),
},
pagesPackages: map[uint][]byte{8: packageBytes},
}
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
if _, err := stateStore.EnsureNodeID(); err != nil {
t.Fatalf("EnsureNodeID failed: %v", err)
}
manager := &fakeManager{currentChecksum: "old-checksum"}
service := New(client, manager, stateStore)
service.SetPagesDir(t.TempDir())
err := service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{Version: "20260309-102", Checksum: "pages-config-checksum"})
if err == nil || !strings.Contains(err.Error(), "escapes deployment root") {
t.Fatalf("expected zip-slip rejection, got %v", err)
}
if len(manager.applyRouteContents) != 0 {
t.Fatalf("OpenResty apply must not run after Pages package rejection")
}
}
func TestSyncOnceRollbackOnNginxFailure(t *testing.T) {
client := &fakeClient{
config: protocol.ActiveConfigResponse{
@@ -239,7 +326,7 @@ func TestSyncOnceRollbackOnNginxFailure(t *testing.T) {
if client.reports[0].MainConfigChecksum == "" || client.reports[0].RouteConfigChecksum == "" {
t.Fatal("expected failed report to include main and route config checksums")
}
if client.reports[0].SupportFileCount != 4 {
if client.reports[0].SupportFileCount != 3 {
t.Fatalf("expected failed report to include support file count, got %d", client.reports[0].SupportFileCount)
}
}
@@ -761,3 +848,76 @@ func TestSyncOnceSkipsFetchWhenHeartbeatChecksumMatches(t *testing.T) {
t.Fatal("expected no apply log when no config change is needed")
}
}
func testPagesPackage(t *testing.T, files map[string]string) []byte {
t.Helper()
var buffer bytes.Buffer
writer := zip.NewWriter(&buffer)
for name, content := range files {
file, err := writer.Create(name)
if err != nil {
t.Fatalf("create zip file failed: %v", err)
}
if _, err := file.Write([]byte(content)); err != nil {
t.Fatalf("write zip file failed: %v", err)
}
}
if err := writer.Close(); err != nil {
t.Fatalf("close zip failed: %v", err)
}
return buffer.Bytes()
}
func testBytesChecksum(data []byte) string {
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
func TestSyncOnceDownloadsPagesDeploymentWithTopLevelFolder(t *testing.T) {
packageBytes := testPagesPackage(t, map[string]string{
"Speed-Test-source/index.html": "hello html",
"Speed-Test-source/assets/app.js": "hello js",
})
checksum := testBytesChecksum(packageBytes)
client := &fakeClient{
config: protocol.ActiveConfigResponse{
Version: "20260309-105",
Checksum: "pages-config-checksum",
SourceConfigJSON: testPagesSourceConfigJSON(77, checksum),
CreatedAt: time.Now().Format(time.RFC3339),
},
pagesPackages: map[uint][]byte{77: packageBytes},
}
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
nodeID, err := stateStore.EnsureNodeID()
if err != nil {
t.Fatalf("EnsureNodeID failed: %v", err)
}
snapshot, _ := stateStore.Load()
snapshot.NodeID = nodeID
if err = stateStore.Save(snapshot); err != nil {
t.Fatalf("save state failed: %v", err)
}
manager := &fakeManager{currentChecksum: "old-checksum"}
service := New(client, manager, stateStore)
pagesDir := t.TempDir()
service.SetPagesDir(pagesDir)
if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{Version: "20260309-105", Checksum: "pages-config-checksum"}); err != nil {
t.Fatalf("SyncOnce failed: %v", err)
}
data, err := os.ReadFile(filepath.Join(pagesDir, "deployments", "77", "current", "index.html"))
if err != nil {
t.Fatalf("expected Pages index.html file to be extracted: %v", err)
}
if string(data) != "hello html" {
t.Fatalf("unexpected Pages index.html content: %s", string(data))
}
jsData, err := os.ReadFile(filepath.Join(pagesDir, "deployments", "77", "current", "assets", "app.js"))
if err != nil {
t.Fatalf("expected Pages assets/app.js file to be extracted: %v", err)
}
if string(jsData) != "hello js" {
t.Fatalf("unexpected Pages assets/app.js content: %s", string(jsData))
}
}
+26 -110
View File
@@ -2,165 +2,81 @@ package wsclient
import (
"context"
"errors"
"log/slog"
"net"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/net/websocket"
"openflare-agent/internal/protocol"
shared "openflare/utils/wsclient"
)
type WSMessage = shared.WSMessage
type MessageHandler = shared.MessageHandler
type Client struct {
baseURL string
token string
timeout time.Duration
sharedClient *shared.Client
}
type Connection struct {
conn *websocket.Conn
url string
readTimeout time.Duration
sharedConn *shared.Connection
}
func New(baseURL string, token string, timeout time.Duration) *Client {
return &Client{
baseURL: strings.TrimRight(baseURL, "/"),
token: strings.TrimSpace(token),
timeout: timeout,
sharedClient: shared.New(shared.Config{
BaseURL: baseURL,
Token: token,
Timeout: timeout,
HeaderKey: "X-Agent-Token",
WSPath: "/api/agent/ws",
}),
}
}
func (c *Client) SetToken(token string) {
c.token = strings.TrimSpace(token)
slog.Debug("agent ws client token updated")
c.sharedClient.SetToken(token)
}
func (c *Client) URL() string {
wsURL, err := buildWebsocketURL(c.baseURL)
if err != nil {
return ""
}
return wsURL
return c.sharedClient.URL()
}
func (c *Client) Connect(ctx context.Context) (protocol.WebSocketConnection, error) {
wsURL, err := buildWebsocketURL(c.baseURL)
conn, err := c.sharedClient.Connect(ctx)
if err != nil {
return nil, err
}
if strings.TrimSpace(c.token) == "" {
return nil, errors.New("agent ws token is empty")
}
origin := strings.TrimSpace(c.baseURL)
if origin == "" {
origin = "http://localhost"
}
config, err := websocket.NewConfig(wsURL, origin)
if err != nil {
return nil, err
}
config.Header = http.Header{}
config.Header.Set("X-Agent-Token", c.token)
if c.timeout > 0 {
config.Dialer = &net.Dialer{Timeout: c.timeout}
}
slog.Debug("agent ws dialing server", "url", wsURL)
conn, err := config.DialContext(ctx)
if err != nil {
return nil, err
}
slog.Debug("agent ws dial succeeded", "url", wsURL)
return &Connection{conn: conn, url: wsURL, readTimeout: websocketReadTimeout(c.timeout)}, nil
}
func buildWebsocketURL(baseURL string) (string, error) {
parsed, err := url.Parse(strings.TrimRight(baseURL, "/"))
if err != nil {
return "", err
}
switch parsed.Scheme {
case "http":
parsed.Scheme = "ws"
case "https":
parsed.Scheme = "wss"
case "ws", "wss":
default:
return "", errors.New("server_url scheme must be http, https, ws, or wss")
}
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/api/agent/ws"
parsed.RawQuery = ""
parsed.Fragment = ""
return parsed.String(), nil
return &Connection{sharedConn: conn}, nil
}
func (conn *Connection) URL() string {
if conn == nil {
if conn == nil || conn.sharedConn == nil {
return ""
}
return conn.url
return conn.sharedConn.URL
}
func (conn *Connection) SendStatus(payload protocol.NodePayload) error {
if conn == nil || conn.conn == nil {
return errors.New("agent ws connection is nil")
}
slog.Debug("agent ws sending status",
"node_id", payload.NodeID,
"current_version", payload.CurrentVersion,
"openresty_status", payload.OpenrestyStatus,
)
return websocket.JSON.Send(conn.conn, protocol.WSOutboundMessage{
Type: protocol.WSMessageTypeStatus,
Payload: payload,
})
return conn.sharedConn.SendMessage(protocol.WSMessageTypeStatus, payload)
}
func (conn *Connection) SendPong() error {
if conn == nil || conn.conn == nil {
return errors.New("agent ws connection is nil")
}
slog.Debug("agent ws sending pong")
return websocket.JSON.Send(conn.conn, protocol.WSOutboundMessage{
Type: protocol.WSMessageTypePong,
})
return conn.sharedConn.SendMessage(protocol.WSMessageTypePong, nil)
}
func (conn *Connection) Receive() (protocol.WSMessage, error) {
var message protocol.WSMessage
if conn == nil || conn.conn == nil {
return message, errors.New("agent ws connection is nil")
}
if conn.readTimeout > 0 {
_ = conn.conn.SetReadDeadline(time.Now().Add(conn.readTimeout))
}
err := websocket.JSON.Receive(conn.conn, &message)
if err != nil {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
slog.Debug("agent ws receive timeout waiting for server message", "timeout", conn.readTimeout)
}
if err := conn.sharedConn.Receive(&message); err != nil {
return message, err
}
slog.Debug("agent ws received message", "type", message.Type)
return message, nil
}
func websocketReadTimeout(requestTimeout time.Duration) time.Duration {
timeout := requestTimeout * 6
if timeout < 75*time.Second {
return 75 * time.Second
}
return timeout
func (conn *Connection) RunReceiveLoop(ctx context.Context, handler shared.MessageHandler) error {
return conn.sharedConn.RunReceiveLoop(ctx, handler)
}
func (conn *Connection) Close() error {
if conn == nil || conn.conn == nil {
if conn == nil || conn.sharedConn == nil {
return nil
}
return conn.conn.Close()
return conn.sharedConn.Close()
}
+11 -3
View File
@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1.7
ARG VERSION=dev
FROM golang:1.25-alpine AS builder
@@ -5,13 +6,19 @@ FROM golang:1.25-alpine AS builder
ARG VERSION
WORKDIR /build
COPY openflare_server/go.mod openflare_server/go.sum /openflare_server/
COPY openflare_relay/go.mod openflare_relay/go.sum /openflare_relay/
WORKDIR /openflare_relay
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY openflare_relay/go.mod openflare_relay/go.sum ./
WORKDIR /build
COPY openflare_server /openflare_server
COPY openflare_relay /openflare_relay
WORKDIR /openflare_relay
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags "-s -w -X 'openflare-relay/internal/config.Version=$VERSION'" -o openflare-relay ./cmd/relay
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags "-s -w -X 'openflare-relay/internal/config.Version=$VERSION'" -o openflare-relay ./cmd/relay
# Final runtime image
FROM fatedier/frps:v0.69.0
@@ -25,3 +32,4 @@ ENV OPENFLARE_FRPS_PATH=/usr/bin/frps
ENV OPENFLARE_DATA_DIR=/var/lib/openflare-relay
ENTRYPOINT ["/usr/local/bin/openflare-relay"]
+1 -1
View File
@@ -45,7 +45,7 @@ func main() {
stateStore := state.NewStore(cfg.StatePath)
_ = stateStore // In the future we may use stateStore for auth caching
frpsManager := frps.NewManager(cfg.FrpsPath, cfg.DataDir)
frpsManager := frps.NewManager(cfg.FrpsPath, cfg.DataDir, cfg.InitialAuthToken())
slog.Info("detected frps version", "version", frpsManager.GetVersion())
+23 -20
View File
@@ -1,61 +1,64 @@
module openflare-relay
go 1.25.0
go 1.25.7
replace openflare => ../openflare_server
require (
golang.org/x/net v0.55.0
openflare v0.0.0-00010101000000-000000000000
)
require openflare v0.0.0-00010101000000-000000000000
require (
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/bytedance/sonic v1.11.2 // indirect
github.com/bytedance/sonic v1.12.9 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/expr-lang/expr v1.17.8 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/glebarez/sqlite v1.11.0 // indirect
github.com/go-acme/lego/v4 v4.35.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/go-playground/validator/v10 v10.25.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/pgx/v5 v5.9.2 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/longbridgeapp/sqlparser v0.3.1 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pressly/goose/v3 v3.27.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.7.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.14.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
@@ -65,8 +68,8 @@ require (
gorm.io/driver/postgres v1.6.0 // indirect
gorm.io/gorm v1.25.10 // indirect
gorm.io/sharding v0.6.2 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
modernc.org/libc v1.72.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.49.1 // indirect
)
+72 -43
View File
@@ -1,20 +1,19 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A=
github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ=
github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -33,10 +32,10 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
@@ -51,29 +50,31 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -83,8 +84,8 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@@ -98,6 +99,8 @@ github.com/longbridgeapp/sqlparser v0.3.1 h1:iWOZWGIFgQrJRgobLXUNJdvqGRpbVXkyKUK
github.com/longbridgeapp/sqlparser v0.3.1/go.mod h1:GIHaUq8zvYyHLCLMJJykx1CdM6LHtkUih/QaJXySSx4=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -106,6 +109,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
@@ -114,34 +119,40 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4=
github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
@@ -152,7 +163,6 @@ golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
@@ -183,13 +193,32 @@ gorm.io/plugin/dbresolver v1.5.1 h1:s9Dj9f7r+1rE3nx/Ywzc85nXptUEaeOO0pt27xdopM8=
gorm.io/plugin/dbresolver v1.5.1/go.mod h1:l4Cn87EHLEYuqUncpEeTC2tTJQkjngPSD+lo8hIvcT0=
gorm.io/sharding v0.6.2 h1:V9inmbdhN+RfWPEKTvbKKKv7qxLz1CneBDQvuL5P7jg=
gorm.io/sharding v0.6.2/go.mod h1:dXaAZv0qyUmLkLAciQ+NH2O1D1A4/ttrrZ/XK4xW9HU=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
modernc.org/cc/v4 v4.28.1 h1:XpLbkYVQ24E8tX5u8+yWGvaxerxkR/S4zqxI8ZoSBuc=
modernc.org/cc/v4 v4.28.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.33.0 h1:dspBCm75jsj8Y/ufwAMVfe375L2iYdMyQ2QG/v3hL54=
modernc.org/ccgo/v4 v4.33.0/go.mod h1:+RhXBoRYzRwaH21mV/aj6XvQRDtfjcZfAlPMsQo8CR0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0=
modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+162 -67
View File
@@ -19,6 +19,8 @@ type Manager struct {
frpsPath string
dataDir string
configPath string
pidPath string
agentToken string
mu sync.RWMutex
activeConfig *service.RelayConfig
@@ -34,15 +36,19 @@ type RuntimeStatus struct {
LastError string
Connections int
ProxyCount int
ClientCount int
Proxies []service.RelayProxyStat
ProcessAlive bool
}
func NewManager(frpsPath string, dataDir string) *Manager {
func NewManager(frpsPath string, dataDir string, agentToken string) *Manager {
return &Manager{
frpsPath: frpsPath,
dataDir: dataDir,
configPath: filepath.Join(dataDir, "frps.toml"),
status: "unhealthy",
pidPath: filepath.Join(dataDir, "frps.pid"),
status: "unknown", // 启动阶段尚未获取配置,状态未知;避免首次 heartbeat 误报 frps_unhealthy
agentToken: agentToken,
}
}
@@ -67,13 +73,19 @@ func (m *Manager) GetStatus() string {
func (m *Manager) GetRuntimeStatus() RuntimeStatus {
m.mu.RLock()
defer m.mu.RUnlock()
status := m.status
lastError := m.lastError
cmd := m.cmd
m.mu.RUnlock()
return RuntimeStatus{
Status: m.status,
LastError: m.lastError,
Status: status,
LastError: lastError,
Connections: 0,
ProxyCount: 0,
ProcessAlive: m.cmd != nil && m.cmd.Process != nil,
ClientCount: 0,
Proxies: nil,
ProcessAlive: cmd != nil && cmd.Process != nil,
}
}
@@ -88,22 +100,36 @@ func (m *Manager) UpdateConfig(cfg *service.RelayConfig) {
if m.activeConfig != nil &&
m.activeConfig.BindPort == cfg.BindPort &&
m.activeConfig.VhostHTTPPort == cfg.VhostHTTPPort &&
m.activeConfig.AuthToken == cfg.AuthToken {
m.activeConfig.AuthToken == cfg.AuthToken &&
m.activeConfig.WebServerEnabled == cfg.WebServerEnabled {
if m.cmd == nil && !m.stopping {
slog.Warn("frps config unchanged but process is not running, restarting")
if err := m.restartProcess(); err != nil {
m.stopping = false
m.generation++
generation := m.generation
if err := m.renderConfig(cfg); err != nil {
slog.Error("failed to render frps config", "error", err)
m.status = "unhealthy"
m.lastError = err.Error()
slog.Error("failed to restart frps with unchanged config", "error", err)
return
}
go m.supervise(generation)
}
return
}
m.activeConfig = cfg
m.stopping = false
m.generation++
generation := m.generation
slog.Info("relay config updated, reloading frps")
if m.cmd != nil && m.cmd.Process != nil {
slog.Debug("stopping existing frps process")
_ = m.cmd.Process.Kill()
m.cmd = nil
}
if err := m.renderConfig(cfg); err != nil {
slog.Error("failed to render frps config", "error", err)
m.status = "unhealthy"
@@ -111,14 +137,7 @@ func (m *Manager) UpdateConfig(cfg *service.RelayConfig) {
return
}
if err := m.restartProcess(); err != nil {
slog.Error("failed to restart frps", "error", err)
m.status = "unhealthy"
m.lastError = err.Error()
} else {
m.status = "healthy"
m.lastError = ""
}
go m.supervise(generation)
}
func (m *Manager) renderConfig(cfg *service.RelayConfig) error {
@@ -136,66 +155,119 @@ func (m *Manager) renderConfig(cfg *service.RelayConfig) error {
buf.WriteString(fmt.Sprintf("token = \"%s\"\n", cfg.AuthToken))
}
// WebServer configuration
buf.WriteString("\n[webServer]\n")
if cfg.WebServerEnabled {
buf.WriteString("addr = \"0.0.0.0\"\n")
} else {
buf.WriteString("addr = \"127.0.0.1\"\n")
}
buf.WriteString(fmt.Sprintf("port = %d\n", 17500))
buf.WriteString("user = \"admin\"\n")
password := m.agentToken
if password == "" {
password = "admin"
}
buf.WriteString(fmt.Sprintf("password = \"%s\"\n", password))
return os.WriteFile(m.configPath, buf.Bytes(), 0644)
}
func (m *Manager) restartProcess() error {
m.generation++
generation := m.generation
if m.cmd != nil && m.cmd.Process != nil {
slog.Debug("stopping existing frps process")
_ = m.cmd.Process.Kill()
m.cmd = nil
}
return m.startProcessLocked(generation)
}
func (m *Manager) supervise(generation uint64) {
backoff := 1 * time.Second
const maxBackoff = 60 * time.Second
func (m *Manager) startProcessLocked(generation uint64) error {
cmd := exec.Command(m.frpsPath, "-c", m.configPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return err
}
m.cmd = cmd
m.status = "healthy"
m.lastError = ""
go func(c *exec.Cmd) {
err := c.Wait()
slog.Warn("frps process exited", "error", err)
for {
m.mu.Lock()
if m.cmd == c {
if m.stopping || m.generation != generation {
m.mu.Unlock()
return
}
ensureNoOrphanProcess(m.pidPath)
cmd := exec.Command(m.frpsPath, "-c", m.configPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
m.status = "unhealthy"
m.lastError = fmt.Sprintf("failed to start: %v", err)
slog.Error("failed to start frps", "error", err, "generation", generation)
m.mu.Unlock()
if !m.sleepOrInterrupt(generation, backoff) {
return
}
backoff = backoff * 2
if backoff > maxBackoff {
backoff = maxBackoff
}
continue
}
_ = os.WriteFile(m.pidPath, []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644)
m.cmd = cmd
m.status = "healthy"
m.lastError = ""
m.mu.Unlock()
startedAt := time.Now()
waitErr := cmd.Wait()
_ = os.Remove(m.pidPath)
m.mu.Lock()
if m.cmd == cmd {
m.cmd = nil
m.status = "unhealthy"
if err != nil {
m.lastError = err.Error()
if waitErr != nil {
m.lastError = fmt.Sprintf("exited with error: %v", waitErr)
} else {
m.lastError = "frps process exited"
m.lastError = "exited unexpectedly"
}
slog.Warn("frps process exited unexpectedly", "error", waitErr, "generation", generation)
}
shouldContinue := !m.stopping && m.generation == generation
m.mu.Unlock()
if !shouldContinue {
return
}
if time.Since(startedAt) >= 10*time.Second {
backoff = 1 * time.Second
}
if !m.sleepOrInterrupt(generation, backoff) {
return
}
backoff = backoff * 2
if backoff > maxBackoff {
backoff = maxBackoff
}
}
}
func (m *Manager) sleepOrInterrupt(generation uint64, d time.Duration) bool {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
deadline := time.Now().Add(d)
for time.Now().Before(deadline) {
select {
case <-ticker.C:
m.mu.RLock()
interrupted := m.stopping || m.generation != generation
m.mu.RUnlock()
if interrupted {
return false
}
}
shouldRestart := !m.stopping && m.generation == generation
m.mu.Unlock()
if !shouldRestart {
return
}
time.Sleep(2 * time.Second)
m.mu.Lock()
defer m.mu.Unlock()
if m.stopping || m.generation != generation {
return
}
slog.Warn("restarting frps after unexpected exit")
if err := m.startProcessLocked(generation); err != nil {
m.status = "unhealthy"
m.lastError = err.Error()
slog.Error("failed to auto restart frps", "error", err)
}
}(cmd)
return nil
}
return true
}
func (m *Manager) Stop() {
@@ -207,5 +279,28 @@ func (m *Manager) Stop() {
_ = m.cmd.Process.Kill()
m.cmd = nil
}
_ = os.Remove(m.pidPath)
m.status = "unhealthy"
}
func ensureNoOrphanProcess(pidPath string) {
data, err := os.ReadFile(pidPath)
if err != nil {
return
}
var pid int
if _, err := fmt.Sscanf(string(data), "%d", &pid); err != nil {
return
}
if pid <= 0 {
return
}
process, err := os.FindProcess(pid)
if err == nil && process != nil {
slog.Warn("attempting to kill potentially orphan process", "pid", pid, "pid_path", pidPath)
_ = process.Kill()
// Wait a little bit to ensure the OS has reclaimed ports
time.Sleep(500 * time.Millisecond)
}
_ = os.Remove(pidPath)
}
@@ -0,0 +1,345 @@
package frps
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
"openflare/service"
)
// Helper to write control file for the dummy script
func writeControl(t *testing.T, dir string, exitCode int, delaySeconds int) {
controlPath := filepath.Join(dir, "control.txt")
content := fmt.Sprintf("%d %d\n", exitCode, delaySeconds)
err := os.WriteFile(controlPath, []byte(content), 0644)
if err != nil {
t.Fatalf("failed to write control file: %v", err)
}
}
// Setup a dummy executable script that reads control.txt to decide exit code and sleep duration
func setupDummyScript(t *testing.T) (string, string) {
dir := t.TempDir()
scriptPath := filepath.Join(dir, "dummy_frps")
// On macOS/Linux, we write a shell script
scriptContent := fmt.Sprintf(`#!/bin/sh
control_file="%s/control.txt"
EXIT_CODE=0
DELAY=0
if [ -f "$control_file" ]; then
read -r EXIT_CODE DELAY < "$control_file"
fi
if [ -n "$DELAY" ] && [ "$DELAY" -gt 0 ] 2>/dev/null; then
sleep "$DELAY"
fi
exit "${EXIT_CODE:-0}"
`, dir)
err := os.WriteFile(scriptPath, []byte(scriptContent), 0755)
if err != nil {
t.Fatalf("failed to write dummy script: %v", err)
}
return scriptPath, dir
}
// Helper to poll for status to eliminate timing flakiness in tests
func assertStatusEventually(t *testing.T, m *Manager, expectedStatus string, timeout time.Duration) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
rt := m.GetRuntimeStatus()
if rt.Status == expectedStatus {
return
}
time.Sleep(50 * time.Millisecond)
}
rt := m.GetRuntimeStatus()
t.Fatalf("expected status eventually %s, got %s (err: %s)", expectedStatus, rt.Status, rt.LastError)
}
func assertCommandExitedEventually(t *testing.T, cmd *exec.Cmd, timeout time.Duration) {
t.Helper()
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case <-time.After(timeout):
t.Fatalf("expected process pid=%d to exit within %s", cmd.Process.Pid, timeout)
case <-done:
}
}
func TestStartProcessSuccess(t *testing.T) {
scriptPath, dir := setupDummyScript(t)
writeControl(t, dir, 0, 5) // exit code 0, sleep 5s
m := NewManager(scriptPath, dir, "agent-token")
defer m.Stop()
cfg := &service.RelayConfig{
BindPort: 7000,
VhostHTTPPort: 8080,
AuthToken: "test-auth",
WebServerEnabled: false,
}
m.UpdateConfig(cfg)
assertStatusEventually(t, m, "healthy", 2*time.Second)
rt := m.GetRuntimeStatus()
if !rt.ProcessAlive {
t.Error("expected process to be alive")
}
}
func TestStartProcessFailureAndBackoff(t *testing.T) {
dir := t.TempDir()
invalidScriptPath := filepath.Join(dir, "non_existent_frps")
m := NewManager(invalidScriptPath, dir, "agent-token")
defer m.Stop()
cfg := &service.RelayConfig{
BindPort: 7000,
VhostHTTPPort: 8080,
AuthToken: "test-auth",
WebServerEnabled: false,
}
m.UpdateConfig(cfg)
assertStatusEventually(t, m, "unhealthy", 2*time.Second)
rt := m.GetRuntimeStatus()
if !strings.Contains(rt.LastError, "failed to start") {
t.Errorf("expected error message containing 'failed to start', got %s", rt.LastError)
}
// Correct the path to dummy script
scriptPath, _ := setupDummyScript(t)
writeControl(t, filepath.Dir(scriptPath), 0, 5)
m.mu.Lock()
m.frpsPath = scriptPath
m.mu.Unlock()
// Wait for backoff retry (1s backoff)
assertStatusEventually(t, m, "healthy", 3*time.Second)
rt = m.GetRuntimeStatus()
if !rt.ProcessAlive {
t.Error("expected process to be alive now")
}
}
func TestUnexpectedExitAndAutorestart(t *testing.T) {
scriptPath, dir := setupDummyScript(t)
// Start with immediate exit code 1
writeControl(t, dir, 1, 0)
m := NewManager(scriptPath, dir, "agent-token")
defer m.Stop()
cfg := &service.RelayConfig{
BindPort: 7000,
VhostHTTPPort: 8080,
AuthToken: "test-auth",
WebServerEnabled: false,
}
m.UpdateConfig(cfg)
assertStatusEventually(t, m, "unhealthy", 2*time.Second)
rt := m.GetRuntimeStatus()
if !strings.Contains(rt.LastError, "exited with error") {
t.Errorf("expected exit error, got %s", rt.LastError)
}
// Change control to be healthy (runs for 5s, exit 0)
writeControl(t, dir, 0, 5)
// Wait for the retry to fire (backoff was 1s)
assertStatusEventually(t, m, "healthy", 3*time.Second)
}
func TestBackoffReset(t *testing.T) {
scriptPath, dir := setupDummyScript(t)
// Rapid exit to increase backoff
writeControl(t, dir, 1, 0)
m := NewManager(scriptPath, dir, "agent-token")
defer m.Stop()
cfg := &service.RelayConfig{
BindPort: 7000,
VhostHTTPPort: 8080,
AuthToken: "test-auth",
WebServerEnabled: false,
}
m.UpdateConfig(cfg)
// Crashed once, backoff is 2s
assertStatusEventually(t, m, "unhealthy", 2*time.Second)
// Now make it run successfully for 11 seconds (exit code 0, sleep 11s)
writeControl(t, dir, 0, 11)
// Wait for next retry to start running
assertStatusEventually(t, m, "healthy", 4*time.Second)
// Wait for process to run for 10.5 seconds to trigger backoff reset
time.Sleep(10500 * time.Millisecond)
// Now make it crash again (exit code 1, sleep 0s)
writeControl(t, dir, 1, 0)
// Wait for it to finish and crash
assertStatusEventually(t, m, "unhealthy", 3*time.Second)
// It crashed. Since it ran for > 10s, backoff should have been reset to 1s.
// We make it healthy again (exit code 0, sleep 5)
writeControl(t, dir, 0, 5)
// Wait 1.5 seconds. If backoff was reset to 1s, it should be healthy now.
assertStatusEventually(t, m, "healthy", 2*time.Second)
}
func TestImmediateRestartOnSameConfigDeadProcess(t *testing.T) {
scriptPath, dir := setupDummyScript(t)
// Crashes immediately
writeControl(t, dir, 1, 0)
m := NewManager(scriptPath, dir, "agent-token")
defer m.Stop()
cfg := &service.RelayConfig{
BindPort: 7000,
VhostHTTPPort: 8080,
AuthToken: "test-auth",
WebServerEnabled: false,
}
m.UpdateConfig(cfg)
// Let it crash
assertStatusEventually(t, m, "unhealthy", 2*time.Second)
// Make it start successfully
writeControl(t, dir, 0, 5)
// Send same config block to trigger immediate restart bypass of backoff sleep
m.UpdateConfig(cfg)
// Check if it started immediately
assertStatusEventually(t, m, "healthy", 2*time.Second)
}
func TestSupervisorGenerationInterrupt(t *testing.T) {
scriptPath, dir := setupDummyScript(t)
writeControl(t, dir, 0, 10)
m := NewManager(scriptPath, dir, "agent-token")
defer m.Stop()
cfg := &service.RelayConfig{
BindPort: 7000,
VhostHTTPPort: 8080,
AuthToken: "test-auth",
WebServerEnabled: false,
}
m.UpdateConfig(cfg)
assertStatusEventually(t, m, "healthy", 2*time.Second)
m.mu.Lock()
gen1 := m.generation
cmd1 := m.cmd
m.mu.Unlock()
if cmd1 == nil {
t.Fatal("expected active process")
}
// Update configuration with new bind port to trigger new generation
cfg2 := &service.RelayConfig{
BindPort: 7001,
VhostHTTPPort: 8080,
AuthToken: "test-auth",
WebServerEnabled: false,
}
m.UpdateConfig(cfg2)
assertStatusEventually(t, m, "healthy", 2*time.Second)
m.mu.Lock()
gen2 := m.generation
cmd2 := m.cmd
m.mu.Unlock()
if gen2 <= gen1 {
t.Errorf("expected generation incremented, got gen1=%d gen2=%d", gen1, gen2)
}
if cmd2 == cmd1 {
t.Error("expected old process killed and new command started")
}
// Verify old process is actually killed
var cmd1Finished int32
go func() {
_ = cmd1.Wait()
atomic.StoreInt32(&cmd1Finished, 1)
}()
time.Sleep(200 * time.Millisecond)
if atomic.LoadInt32(&cmd1Finished) != 1 {
t.Error("expected first process to be killed")
}
}
func TestUpdateConfigKillsOrphanProcessBeforeRestart(t *testing.T) {
scriptPath, dir := setupDummyScript(t)
writeControl(t, dir, 0, 5)
m := NewManager(scriptPath, dir, "agent-token")
defer m.Stop()
orphan := exec.Command("sh", "-c", "sleep 30")
if err := orphan.Start(); err != nil {
t.Fatalf("failed to start orphan process: %v", err)
}
t.Cleanup(func() {
if orphan.Process != nil {
_ = orphan.Process.Kill()
}
})
if err := os.WriteFile(m.pidPath, []byte(fmt.Sprintf("%d", orphan.Process.Pid)), 0o644); err != nil {
t.Fatalf("failed to seed orphan pid file: %v", err)
}
cfg := &service.RelayConfig{
BindPort: 7000,
VhostHTTPPort: 8080,
AuthToken: "test-auth",
WebServerEnabled: false,
}
m.UpdateConfig(cfg)
assertCommandExitedEventually(t, orphan, 2*time.Second)
assertStatusEventually(t, m, "healthy", 2*time.Second)
}
+43 -10
View File
@@ -10,6 +10,7 @@ import (
"openflare-relay/internal/httpclient"
"openflare-relay/internal/observability"
"openflare-relay/internal/state"
"openflare-relay/internal/updater"
"openflare/service"
)
@@ -18,6 +19,7 @@ type Service struct {
frpsManager *frps.Manager
config *config.Config
stateStore *state.Store
updater *updater.Service
}
func New(client *httpclient.Client, manager *frps.Manager, cfg *config.Config, stateStore *state.Store) *Service {
@@ -26,6 +28,7 @@ func New(client *httpclient.Client, manager *frps.Manager, cfg *config.Config, s
frpsManager: manager,
config: cfg,
stateStore: stateStore,
updater: updater.New(),
}
}
@@ -51,16 +54,18 @@ func (s *Service) doHeartbeat(ctx context.Context) {
runtimeStatus := s.frpsManager.GetRuntimeStatus()
payload := service.RelayHeartbeatPayload{
Version: config.Version,
ExtVersion: s.frpsManager.GetVersion(),
RelayStatus: runtimeStatus.Status,
FrpsConnCount: runtimeStatus.Connections,
FrpsProxyCount: runtimeStatus.ProxyCount,
Name: s.config.NodeName,
IP: s.config.NodeIP,
Profile: observability.BuildProfile(s.config, s.stateStore),
Snapshot: observability.BuildSnapshot(s.config, s.stateStore),
HealthEvents: observability.BuildHealthEvents(runtimeStatus),
Version: config.Version,
ExtVersion: s.frpsManager.GetVersion(),
RelayStatus: runtimeStatus.Status,
FrpsConnCount: runtimeStatus.Connections,
FrpsProxyCount: runtimeStatus.ProxyCount,
FrpsClientCount: runtimeStatus.ClientCount,
FrpsProxies: runtimeStatus.Proxies,
Name: s.config.NodeName,
IP: s.config.NodeIP,
Profile: observability.BuildProfile(s.config, s.stateStore),
Snapshot: observability.BuildSnapshot(s.config, s.stateStore),
HealthEvents: observability.BuildHealthEvents(runtimeStatus),
}
resp, err := s.client.Heartbeat(ctx, payload)
@@ -72,4 +77,32 @@ func (s *Service) doHeartbeat(ctx context.Context) {
// Update configs if changed
s.frpsManager.UpdateConfig(resp.RelayConfig)
if resp != nil && resp.RelaySettings != nil {
s.tryAutoUpdate(ctx, resp.RelaySettings)
}
}
func (s *Service) tryAutoUpdate(ctx context.Context, settings *service.RelaySettings) {
if settings == nil || s.updater == nil {
return
}
force := settings.UpdateNow
shouldCheck := settings.AutoUpdate || force
if !shouldCheck || settings.UpdateRepo == "" {
return
}
channel := "stable"
if force && settings.UpdateChannel != "" {
channel = settings.UpdateChannel
}
slog.Info("checking for relay updates", "repo", settings.UpdateRepo, "channel", channel, "force", force)
err := s.updater.CheckAndUpdate(ctx, settings.UpdateRepo, updater.UpdateOptions{
Channel: channel,
TagName: settings.UpdateTag,
Force: force,
})
if err != nil {
slog.Error("relay update check failed", "error", err)
}
}
+25 -56
View File
@@ -51,66 +51,35 @@ func (r *Runner) Run(ctx context.Context) error {
}
}
func (r *Runner) handleConnection(ctx context.Context, conn *wsclient.Connection) {
// Send pings at 2× heartbeat interval to keep the server-side read deadline
// from expiring (server closes the WS if no data arrives within ~30 s).
pingInterval := r.Config.HeartbeatInterval.Duration() * 2
pingTicker := time.NewTicker(pingInterval)
defer pingTicker.Stop()
type relayWSHandler struct {
runner *Runner
}
messages := make(chan service.WSMessage, 8)
readDone := make(chan error, 1)
go func() {
for {
msg, err := conn.Receive()
if err != nil {
readDone <- err
return
}
select {
case messages <- msg:
case <-ctx.Done():
readDone <- ctx.Err()
return
}
}
}()
func (h *relayWSHandler) OnConnect(ctx context.Context) error {
return nil
}
for {
select {
case <-ctx.Done():
return
case err := <-readDone:
slog.Error("relay ws receive failed", "error", err)
return
case <-pingTicker.C:
if err := conn.SendPing(); err != nil {
slog.Error("relay ws send ping failed", "error", err)
return
}
case msg := <-messages:
switch msg.Type {
case "ping":
_ = conn.SendPong()
case "pong":
slog.Debug("relay ws pong received")
case "relay_config":
payloadBytes, ok := msg.Payload.(json.RawMessage)
if !ok {
slog.Error("invalid relay_config payload type")
continue
}
var cfg service.RelayConfig
if err := json.Unmarshal(payloadBytes, &cfg); err != nil {
slog.Error("failed to unmarshal relay_config", "error", err)
continue
}
r.FrpsManager.UpdateConfig(&cfg)
default:
slog.Debug("ignored unknown ws message type", "type", msg.Type)
}
func (h *relayWSHandler) HandleMessage(ctx context.Context, msg wsclient.WSMessage) error {
switch msg.Type {
case "relay_config":
var cfg service.RelayConfig
if err := json.Unmarshal(msg.Payload, &cfg); err != nil {
slog.Error("failed to unmarshal relay_config", "error", err)
return nil
}
h.runner.FrpsManager.UpdateConfig(&cfg)
default:
slog.Debug("ignored unknown ws message type", "type", msg.Type)
}
return nil
}
func (h *relayWSHandler) OnClose(err error) {
slog.Error("relay ws receive failed", "error", err)
}
func (r *Runner) handleConnection(ctx context.Context, conn *wsclient.Connection) {
_ = conn.RunReceiveLoop(ctx, &relayWSHandler{runner: r})
}
func (r *Runner) sleepContext(ctx context.Context, d time.Duration) {
@@ -0,0 +1,51 @@
//go:build !windows
package updater
import (
"fmt"
"log/slog"
"os"
"syscall"
)
func replaceAndRestart(execPath string, tmpPath string) error {
backupPath := execPath + ".bak"
if err := removeBackupBinary(backupPath); err != nil {
return err
}
if err := os.Rename(execPath, backupPath); err != nil {
renameErr := err
if err := os.Remove(tmpPath); err != nil && !os.IsNotExist(err) {
slog.Error("remove tmp binary failed", "path", tmpPath, "error", err)
return fmt.Errorf("backup current binary: %w; remove tmp binary: %v", renameErr, err)
}
return fmt.Errorf("backup current binary: %w", renameErr)
}
if err := os.Rename(tmpPath, execPath); err != nil {
replaceErr := err
if err := os.Rename(backupPath, execPath); err != nil {
slog.Error("restore backup binary failed", "path", backupPath, "error", err)
return fmt.Errorf("replace binary: %w; restore backup binary: %v", replaceErr, err)
}
return fmt.Errorf("replace binary: %w", replaceErr)
}
if err := removeBackupBinary(backupPath); err != nil {
return err
}
if err := syscall.Exec(execPath, os.Args, os.Environ()); err != nil {
return fmt.Errorf("exec restart: %w", err)
}
return fmt.Errorf("unreachable after exec")
}
func removeBackupBinary(path string) error {
if err := os.Remove(path); err != nil {
if os.IsNotExist(err) {
return nil
}
slog.Error("remove backup binary failed", "path", path, "error", err)
return err
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More