feat(backend): 挂载 /v1 API + 实现 nodes/connect 端到端

- 新增 internal/dpcred 包,统一 DeriveHy2Password + DefaultFlow
  agentd 与 HTTP connect handler 共享同一实现
- 新增迁移 000011:nodes 表拆分 reality_prk 私钥 / reality_pbk 公钥
  reality_short_id;修正 handler_grpc.go 使用私钥字段
- 新增迁移 000012:connect_credentials 持久化凭证
  实现 CredentialsForNode 修复 agent 重连 resync 原先返回空的桩
- 扩展 NodeStore 接口:ListUp / EntitlementForUser /
  PersistCredential / DeleteCredential;同步 grpc_test.go mock
- 新增 httpapi/nodes.go:GET /nodes、POST /nodes/id/connect
  Hub.Push + PersistCredential + 渲染完整 sing-box client 配置 JSON
  POST /nodes/id/disconnect
- 新增 httpapi/account.go:GET /me、GET /plans、GET /notices
- 新增 httpapi/clientconfig.go:BuildClientConfig 服务端渲染
- 重写 cmd/server/main.go:手写 chi public/protected 分组
  nodes.Service/Hub 在 main 构造并共享;SMTPMailer/LogMailer

go build ./... && go vet ./... && go test ./... 全部通过

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-15 23:09:56 +08:00
parent b35bfe10dc
commit cadd527680
14 changed files with 1017 additions and 149 deletions
+6 -29
View File
@@ -1,34 +1,11 @@
package agentd
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
)
import "github.com/wangjia/pangolin/server/internal/dpcred"
// DeriveHy2Password derives a node-agnostic Hysteria2 password from the opaque
// data-plane credential id (dp_uuid). The REALITY inbound uses dp_uuid directly as
// the VLESS user uuid; the Hy2 inbound cannot reuse a UUID as a password verbatim
// (it must look like an opaque secret), so it is derived from the SAME source —
// "password = dp_uuid 同源派生" — via a keyed HMAC.
//
// The derivation is deterministic given (dp_uuid, key): the control plane runs the
// exact same function when it builds the client's connect config (doc/02 §3.1), so
// both sides agree without the password ever crossing the agent contract.
//
// Risk note (carried from task #5): the derivation key is shared material. If a
// node is seized the attacker still only sees dp_uuids and this node's key, which
// lets them recompute Hy2 passwords for dp_uuids THEY ALREADY HOLD — it does not
// reveal other subscribers' credentials or any account identity. Rotating the key
// rotates every Hy2 password. When key == "" the password falls back to the raw
// dp_uuid (acceptable for dev; production cloud-init always injects a key).
// DeriveHy2Password is an alias for dpcred.DeriveHy2Password kept here so
// existing callers within the agentd package compile without change.
// The canonical implementation lives in internal/dpcred so that both the node
// agent and the HTTP connect handler share the exact same derivation logic.
func DeriveHy2Password(dpUUID, key string) string {
if key == "" {
return dpUUID
}
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(dpUUID))
sum := mac.Sum(nil)
// base64url without padding → URL/JSON-safe, 43 chars.
return base64.RawURLEncoding.EncodeToString(sum)
return dpcred.DeriveHy2Password(dpUUID, key)
}