feederback

module
v0.0.0-...-1b929c4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: MIT

README

feederback — Go Feedback 系统

Go 服务端 + Go 客户端 SDK + 原生 Webpanel。提交反馈时执行 PoW(工作量证明,按本机校准约 30 秒),服务端用 Ed25519 签名每条记录;HTTP + protobuf 传输;从 nginx 头部取 IP 做自动限流;按最大存储字节滚动剔除旧数据。

特性

  • PoW 防滥用:SHA-256 前导零,难度按本机校准(cmd/benchpow),默认约 30 秒;challenge 一次性 + TTL + 上限防 DoS
  • Ed25519 签名:每条记录签名(覆盖产品 ID、二进制指纹等全部提交字段),公钥随响应下发,客户端本地验签,防篡改
  • 多产品区分:面板创建产品(自定义字符 ID,如 game-app),各产品独立配额与滚动剔除;SDK 按产品 ID 提交,面板按产品筛选
  • 可执行文件指纹:SDK 上传时自动计算运行中可执行文件(可附加文件)的 sha256,随反馈提交,可直接 sha256sum 校验,定位故障来自哪个构建版本
  • 自动限流:每个 IP 10/min、每个机器码 2/min(滑动窗口),IP 取自 nginx 透传头部(trust 门控防伪造)
  • 字段限制:logs ≤ 64KB、content ≤ 1KB、os_info ≤ 1KB
  • 滚动剔除MAX_BYTES 容量上限,超出按时间从旧到新删除(每产品独立)
  • 原生 Webpanel:纯 HTML/CSS/JS,管理密码登录,无前端框架
  • 三库支持:sqlite / mysql / postgres,通过 CONNINFO 环境变量选择
  • 多实例扩展BACKEND=redis 让 challenge/限流跨实例共享(接口抽象,可再扩展其他后端)

快速开始

# 校准 PoW 难度(输出推荐值,写回 POW_DIFFICULTY)
go run ./cmd/benchpow

# 启动服务端(sqlite)
CONNINFO=sqlite://./data/feed.db ADMIN_PASSWORD=secret go run ./cmd/server

# 健康检查
curl localhost:8080/healthz
# 公钥(供离线验签)
curl localhost:8080/api/v1/pubkey
# 面板:浏览器打开 http://localhost:8080/panel/
#   → 首次登录后在「产品管理」创建产品,记下自定义字符 ID(如 game-app)

# SDK 提交一条反馈(PoW 默认约 30 秒;SDK 会自动计算本可执行文件的 sha256)
go run ./examples/submit -server http://127.0.0.1:8080 -product <产品ID> "反馈文本" "日志" "Linux x86_64; app v1.0"

# 或用命令行提交工具(make build 产出 bin/submit)
bin/submit -product <产品ID> -content "反馈文本" [-logs-file ./app.log] [-os "Linux; v1.0"]
echo "反馈文本" | bin/submit -product <产品ID>          # 内容可从 stdin 管道输入
bin/submit -product <产品ID> -content "..." -json       # JSON 输出便于脚本化

环境变量

变量 默认 说明
HTTP_ADDR :8080 监听地址
CONNINFO sqlite://./data/feed.db 三库 DSN
DATA_DIR ./data 私钥等文件目录
MAX_BYTES 536870912 (512MiB) 默认滚动剔除容量(每产品配额;产品创建时可单独指定,0=继承此值)
ADMIN_PASSWORD (空) 面板密码;空则登录一律拒绝
SIGNING_PRIVKEY (空) base64(std) 32B seed;优先于文件
PRIVKEY_FILE <DATA_DIR>/ed25519.key 无 env 时自动生成(0600)
POW_DIFFICULTY 26 下发难度;默认按本机校准约 30s,换机器用 benchpow -target 30 重校
POW_MAX_DIFFICULTY 48 拒绝过高难度配置
CHALLENGE_TTL / CHALLENGE_MAX 5m / 100000 challenge 生命周期与上限(需远大于 PoW 时长,默认 30s,留足余量)
RATE_WINDOW 60s 滑动窗口
RATE_IP_SUBMIT / RATE_MACHINE_SUBMIT 10 / 2 submit 限流
RATE_IP_CHALLENGE / RATE_LOGIN 60 / 5 challenge / 登录限流
TRUST_PROXY_HEADERS false 信任 X-Real-IP/X-Forwarded-For
TRUSTED_PROXIES (空) 逗号分隔 CIDR;命中则信任头部
SESSION_SECRET (空) 面板会话密钥;空则启动随机(重启失效)
BACKEND memory 共享后端:memory(单机) / redis(多实例)
REDIS_ADDR 127.0.0.1:6379 Redis/Valkey 地址
REDIS_PASSWORD / REDIS_DB (空)/ 0
REDIS_PREFIX fb Redis key 前缀,多环境隔离
LOG_LEVEL info debug/info/warn/error

数据库

CONNINFO 按 scheme 选择驱动:

# sqlite(pure-Go,无需 cgo)
CONNINFO=sqlite://./data/feed.db
CONNINFO=sqlite://:memory:            # 内存库(测试)

# mysql
CONNINFO=mysql://user:pass@tcp(127.0.0.1:3306)/feederback

# postgres
CONNINFO=postgres://user:pass@127.0.0.1:5432/feederback

首次启动自动建表(AutoMigrate),创建默认产品 default(ID 即 default),并初始化其 total_bytes:{product_id} 滚动剔除计数器;之后面板创建的产品各自独立计数。

多产品

产品在面板「产品管理」创建,SDK 提交时按产品 ID 区分:

  • 自定义字符 ID:创建时设置一个便于记忆的字符 ID(如 game-app),SDK / CLI 的 -product 直接使用它;面板默认产品为 default
  • 独立配额:每个产品有自己的滚动剔除上限(创建时指定 max_bytes,0=继承全局 MAX_BYTES),互不挤占
  • 按产品筛选:面板可按产品过滤反馈、查看单产品统计(/api/panel/stats?product=<id>
  • 默认产品:首次启动自动创建,兼容直接使用

nginx 反代与真实 IP

/api/v1/submit 的 IP 限流依赖真实客户端 IP。在 nginx 前配置:

server {
    listen 8081;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

服务端设置 TRUST_PROXY_HEADERS=true(或 TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8)。 默认不信任头部——直接暴露 8080 时伪造 X-Real-IP 无效,防止绕过 IP 限流。 机器码由客户端生成,客户端可自改,机器码限流为 best-effort。

多实例部署

单实例默认 BACKEND=memory(challenge 与限流为进程内实现)。多实例(横向扩容)时,这些状态必须共享,否则 challenge 在 A 下发、B 收不到,限流被 N 倍绕过:

# 每个实例相同的配置(除 HTTP_ADDR 外)
BACKEND=redis
REDIS_ADDR=127.0.0.1:6379
REDIS_PREFIX=fb
SESSION_SECRET=<固定值,必须所有实例一致>
SIGNING_PRIVKEY=<base64 32B seed,必须所有实例一致>
CONNINFO=mysql://user:pass@tcp(host:3306)/feederback   # 必须共享 DB

多实例硬性要求

要求
数据库 必须共享 CONNINFO=mysql/pg。sqlite 文件多进程有锁风险,:memory: 每实例独立
SESSION_SECRET 所有实例一致;否则 LB 转发后面板登录态失效(redis 模式下为空会启动 WARN)
SIGNING_PRIVKEY 所有实例一致(或用 env seed);否则各实例公钥不同,SDK 验签失败
Redis challenge/限流共享;REDIS_PREFIX 用于多环境隔离

实现原理pow.ChallengeStoreratelimit.RateLimiter 为接口,internal/backend 提供 memory(单机)与 redis(多实例)两套实现,BACKEND 环境变量切换。

  • RedisChallengeStoreSET NX EX 下发 + GETDEL 原子一次性
  • RedisLimiter:ZSET + Lua 原子滑动窗口,多实例并发在 Redis 内串行化
  • Redis 故障时fail-closed(拒绝提交/限流),日志可见
  • 滚动剔除的 total_bytes 计数器在 DB 内原子维护,天然多实例安全

限制说明CHALLENGE_MAX 仅对 memory 后端生效;redis 后端靠共享限流 + TTL 兜底。 各实例时钟需 NTP 同步(限流窗口基于本地时钟)。

产品数据:产品/配额/反馈存于共享数据库,多实例天然一致(Redis 仅负责 challenge 与限流)。

SDK 使用

完整使用文档见 docs/sdk.md(包路径 github.com/cxykevin/feederback/sdk)。

import feedbacksdk "github.com/cxykevin/feederback/sdk"

// 第二个参数为产品 ID(面板创建的自定义字符 ID),必填
client, err := feedbacksdk.New("http://127.0.0.1:8080", "game-app")
if err != nil { /* ... */ }

// ctx 可取消:PoW 求解与网络请求均响应取消
// 上传时自动计算可执行文件(os.Executable)sha256 并随反馈提交
result, err := client.Submit(ctx, content, logs, osInfo)
if err != nil { /* 处理错误 */ }

fmt.Println(result.FeedbackID)  // 已本地验签通过

可执行文件指纹(默认开启):

  • 上传时自动哈希 os.Executable() 的完整内容;单文件场景 binary_hash 即该文件的 sha256sum
  • WithExtraFile(path) 可附加文件一并纳入同一哈希流(按路径排序,结果确定)
  • WithExecutableHash(false) 关闭指纹计算(失败/禁用时相关字段为空串,不影响提交)

选项WithHTTPClientWithTimeoutWithMachineIDPathWithMaxRetriesWithExecutableHashWithExtraFile

  • 机器码:首次生成 UUID 持久化到 os.UserCacheDir()/feederback/machine.id(可用 WithMachineIDPath 覆盖)
  • 重试:429 按 Retry-After 等待、INVALID_CHALLENGE 重取、5xx/网络错误指数退避(WithMaxRetries
  • SDK 不记录 content/logs

限流与 429

限制 key
每个 IP submit 10/min ip:{ip}
每个机器码 submit 2/min mc:{machine_code}
每个 IP challenge 60/min ip:{ip}
面板登录 5/min ip:{ip}

超限返回 HTTP 429 + Retry-After + proto Error{RATE_LIMITED}

协议(proto)

/api/v1/* 均为 protobuf body(Content-Type: application/x-protobuf):

POST /api/v1/challenge → Challenge{challenge_id, challenge, difficulty, expires_at, version}
POST /api/v1/submit    → SubmitRequest{challenge_id, nonce, content, logs, os_info, machine_code,
                                       product_id, binary_hash, binary_path, binary_size}
                        → SubmitResponse{feedback_id, created_at, signature(Ed25519), public_key}
GET  /api/v1/pubkey    → PublicKey{key, issued_at}
失败体 → Error{code, message, retry_after_seconds}

PoW:SHA-256(challenge32 || be64(nonce)) 前导零 bit 数 ≥ difficulty。 签名字节串覆盖 id/content/logs/os_info/machine_code/product_id/binary_hash/created_at,见 internal/sign.CanonicalBytes

开发

make gen      # 重新生成 protobuf 代码
make test     # go test -race ./...
make vet
make build    # 输出 bin/server 与 bin/submit
make bench    # PoW 校准

后台运行服务器(日志 data/server.log,PID data/server.pid):

make start            # 编译并后台启动,等待 /healthz 就绪后返回
make status           # 查看运行状态
make logs             # 跟随日志
make stop             # 按 PID 停止

# 自定义配置:环境变量直接前缀
ADMIN_PASSWORD=secret POW_DIFFICULTY=26 HTTP_ADDR=:8080 make start

目录结构

proto/            # protobuf 定义
gen/              # 生成代码
cmd/server        # 服务端入口
cmd/benchpow      # PoW 难度校准
cmd/submit        # 简易提交 CLI(bin/submit)
internal/config   # env + DSN 三库 + GORM
internal/pow      # PoW 求解/验证 + challenge store
internal/sign     # Ed25519 签名 + 规范字节串
internal/store    # GORM 模型 + 滚动剔除
internal/ratelimit# 滑动窗口限流
internal/server   # HTTP 路由/中间件/handler
internal/webpanel # 原生面板(embed + session + JSON API)
sdk/              # 客户端 SDK
examples/submit   # SDK 演示
cmd/submit        # 提交 CLI(见 cmd/submit)
docs/sdk.md       # SDK 使用文档
test/             # 端到端测试

Directories

Path Synopsis
cmd
benchpow command
benchpow 校准 PoW 难度:测本机 hashrate,推荐 difficulty 使求解约 target 秒。
benchpow 校准 PoW 难度:测本机 hashrate,推荐 difficulty 使求解约 target 秒。
server command
server 启动 Feedback 服务端。
server 启动 Feedback 服务端。
submit command
submit 简易提交 CLI:向 Feedback 服务端提交一条反馈。
submit 简易提交 CLI:向 Feedback 服务端提交一条反馈。
examples
submit command
submit 演示 Feedback SDK 全链路提交。
submit 演示 Feedback SDK 全链路提交。
gen
internal
backend
Package backend 装配多实例可共享的依赖(challenge store + 限流器)。
Package backend 装配多实例可共享的依赖(challenge store + 限流器)。
config
Package config 负责环境变量解析、数据库 DSN 解析与 GORM 初始化。
Package config 负责环境变量解析、数据库 DSN 解析与 GORM 初始化。
pow
Package pow 实现工作量证明:SHA-256(challenge || be64(nonce)) 前导零 bit 数 >= difficulty。
Package pow 实现工作量证明:SHA-256(challenge || be64(nonce)) 前导零 bit 数 >= difficulty。
ratelimit
Package ratelimit 实现滑动窗口限流器(内存版)。
Package ratelimit 实现滑动窗口限流器(内存版)。
server
Package server 实现 HTTP 服务:路由、中间件、/api/v1 handler。
Package server 实现 HTTP 服务:路由、中间件、/api/v1 handler。
sign
Package sign 实现 Ed25519 服务端签名与规范字节串构造。
Package sign 实现 Ed25519 服务端签名与规范字节串构造。
store
Package store 定义 GORM 模型、滚动剔除与查询接口。
Package store 定义 GORM 模型、滚动剔除与查询接口。
webpanel
Package webpanel 实现原生 Webpanel(无前端框架): 管理密码登录 + HMAC 签名 cookie 会话 + JSON API + 内嵌静态资源。
Package webpanel 实现原生 Webpanel(无前端框架): 管理密码登录 + HMAC 签名 cookie 会话 + JSON API + 内嵌静态资源。
Package feedbacksdk 提供 Feedback 客户端 SDK。
Package feedbacksdk 提供 Feedback 客户端 SDK。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL