fix(render,store): frpc user 支持 + 修正 useEncryption/useCompression 渲染位置

- store.Remote 新增 user 字段(DB 列名 usr,避开保留字;migrate() 自动加列),
  渲染为 frpc 全局 user。面板型 frps(如 ChhChongGou 实例)强制要求 user 登录,
  缺失时服务端返回 panel authentication unavailable: panel HTTP status 403
- ★ 修复渲染 bug:useEncryption/useCompression/bandwidthLimit 必须内嵌在 proxy 的
  transport 对象(frpc ProxyTransport)。此前渲染在 proxy 顶层,frpc 默认
  --strict-config 直接拒载:unknown field "useEncryption"。任何启用加密/压缩
  的转发都无法启动。poolCount 无 proxy 级表达(官方 ProxyTransport 无此字段),
  保留在 store 但不再渲染
- 前端:RemoteNode 画布节点与 StatusView 表单加 user 输入(画布此前会静默
  清掉未声明的字段),带『面板型 frps 必填』提示
- 测试:user 往返(含迁移路径)、渲染含/不含 user、transport 内嵌与顶层
  非法键回归守卫
This commit is contained in:
JianFeeeee
2026-10-12 19:16:28 +08:00
parent b29cd0d186
commit d83e5d8e70
7 changed files with 214 additions and 36 deletions

View File

@ -25,13 +25,19 @@ type Proxy struct {
// CustomDomains is used by http/https proxies when set.
CustomDomains []string
// Advanced transport knobs (M1): proxy-level encryption/compression/limit.
// Advanced transport knobs (M1). These are rendered INSIDE the proxy's
// `transport` object (frpc's ProxyTransport), not at the proxy top level.
UseEncryption bool
UseCompression bool
BandwidthLimit string
PoolCount int
Metadatas map[string]string
Annotations map[string]string
// PoolCount has no per-proxy representation in frpc (ProxyTransport has no
// poolCount field; only the global `transport.poolCount` exists), so this
// value is deliberately not rendered — use the remote's TransportPool for
// connection pooling. Kept here so existing canvases keep loading.
PoolCount int
Metadatas map[string]string
Annotations map[string]string
// M2 HTTP/HTTPS routing (http/https only).
Locations []string // path routing
@ -73,9 +79,15 @@ type frpcTLS struct {
ServerName string `json:"serverName,omitempty"`
}
// frpcProxyTransport is the per-proxy transport section (frpc >= 0.52)
// where bandwidthLimit is defined.
// frpcProxyTransport is the per-proxy transport section (frpc >= 0.52).
//
// useEncryption/useCompression/bandwidthLimit all live HERE, not at the proxy's
// top level: frpc's ProxyTransport struct owns them, and frpc runs with
// --strict-config by default, so a top-level spelling fails the whole config
// with `unmarshal ProxyConfig error: json: unknown field "useEncryption"`.
type frpcProxyTransport struct {
UseEncryption bool `json:"useEncryption,omitempty"`
UseCompression bool `json:"useCompression,omitempty"`
BandwidthLimit string `json:"bandwidthLimit,omitempty"`
}
@ -96,13 +108,9 @@ type frpcProxy struct {
CustomDomains []string `json:"customDomains,omitempty"`
SubDomain string `json:"subdomain,omitempty"`
// M1 advanced transport fields. bandwidthLimit lives in transport.
UseEncryption bool `json:"useEncryption,omitempty"`
UseCompression bool `json:"useCompression,omitempty"`
PoolCount int `json:"poolCount,omitempty"`
Metadatas map[string]string `json:"metadatas,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Transport *frpcProxyTransport `json:"transport,omitempty"`
Metadatas map[string]string `json:"metadatas,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Transport *frpcProxyTransport `json:"transport,omitempty"`
// M2 HTTP/HTTPS routing.
Locations []string `json:"locations,omitempty"`
@ -148,8 +156,12 @@ type frpcHTTPHeader struct {
// Config is a complete frpc configuration document.
type Config struct {
ServerAddr string `json:"serverAddr"`
ServerPort int `json:"serverPort"`
ServerAddr string `json:"serverAddr"`
ServerPort int `json:"serverPort"`
// User is frpc's global `user` field. frps prefixes every proxy name with it
// (`<user>.<name>`), and panel-managed frps requires it to admit the login.
// Omitted when empty so plain frps sees an unchanged config.
User string `json:"user,omitempty"`
Auth frpcAuth `json:"auth"`
Transport frpcTransport `json:"transport,omitempty"`
WebServer *frpcWebServer `json:"webServer,omitempty"`
@ -163,6 +175,7 @@ func Render(remote store.Remote, proxies []Proxy) ([]byte, error) {
cfg := Config{
ServerAddr: remote.IP,
ServerPort: remote.Port,
User: remote.User,
Auth: frpcAuth{Method: "token", Token: remote.Token},
LoginFailExit: false,
Proxies: make([]frpcProxy, 0, len(proxies)),
@ -200,17 +213,21 @@ func Render(remote store.Remote, proxies []Proxy) ([]byte, error) {
LocalIP: p.LocalIP,
LocalPort: p.LocalPort,
SubDomain: p.SubDomain,
UseEncryption: p.UseEncryption,
UseCompression: p.UseCompression,
PoolCount: p.PoolCount,
Metadatas: p.Metadatas,
Annotations: p.Annotations,
Locations: p.Locations,
HostHeaderRewrite: p.HostHeaderRewrite,
HTTPHeaders: p.HTTPHeaders,
}
if p.BandwidthLimit != "" {
frpcP.Transport = &frpcProxyTransport{BandwidthLimit: p.BandwidthLimit}
// useEncryption/useCompression/bandwidthLimit are all members of frpc's
// per-proxy `transport` object — emitting them at the proxy top level
// makes frpc reject the entire config under --strict-config.
if p.UseEncryption || p.UseCompression || p.BandwidthLimit != "" {
frpcP.Transport = &frpcProxyTransport{
UseEncryption: p.UseEncryption,
UseCompression: p.UseCompression,
BandwidthLimit: p.BandwidthLimit,
}
}
if p.BasicAuthUser != "" || p.BasicAuthPassword != "" {
frpcP.HTTPBasicAuth = &frpcBasicAuth{User: p.BasicAuthUser, Password: p.BasicAuthPassword}