Bug 描述
当设置 CLIENT_RPC_URL 环境变量(用于反向代理场景,如 wss://domain:443)时,master 内部的 server 和 client 组件错误地使用该外部 URL 连接自身,导致 websocket: bad handshake 错误,服务无法启动。
环境
- frp-panel 版本:v0.1.37
- 部署方式:Docker
network_mode: host
- 反向代理:nginx,gRPC 使用
grpc_pass grpcs://127.0.0.1:9004
复现步骤
- 使用以下 docker-compose 部署 master:
services:
frpp-master:
image: vaalacat/frp-panel:latest
network_mode: host
environment:
APP_GLOBAL_SECRET: your_secret
MASTER_RPC_HOST: 1.2.3.4
MASTER_RPC_PORT: 9004
MASTER_API_HOST: 1.2.3.4
MASTER_API_PORT: 9005
CLIENT_RPC_URL: wss://rpc.example.com:443
CLIENT_API_URL: https://web.example.com:443
volumes:
- ./data:/data
restart: unless-stopped
command: master
- nginx 反代配置(RPC):
server {
listen 443 ssl;
http2 on;
server_name rpc.example.com;
ssl_certificate ...;
ssl_certificate_key ...;
location / {
grpc_pass grpcs://127.0.0.1:9004;
grpc_ssl_name localhost;
grpc_ssl_verify off;
grpc_set_header X-Real-IP $remote_addr;
}
}
- 启动容器
期望行为
master 内部的 server/client 组件应直接连接本地 RPC 端口(MASTER_RPC_HOST:MASTER_RPC_PORT),CLIENT_RPC_URL 仅用于生成外部客户端的连接配置。
实际行为
日志输出:
[error] wsgrpc dialer error: websocket: bad handshake
[error] rpc error: code = Unavailable desc = connection error: transport: Error while dialing: websocket: bad handshake
[GIN] 400 | GET "/wsgrpc"
内部 server 组件尝试通过 wss://rpc.example.com:443 连接,nginx 将请求转发到本地 gRPC 端口,但本地端口只接受原生 gRPC,不接受 WebSocket,导致握手失败。容器陷入无限重启。
根因分析
conf/helper.go 中的 GetRPCConnInfo() 始终优先使用 cfg.Client.RPCUrl(来自 CLIENT_RPC_URL 环境变量),而非本地 MASTER_RPC_HOST:MASTER_RPC_PORT:
func GetRPCConnInfo(cfg Config) ConnInfo {
rpcUrl := cfg.Client.RPCUrl // 读取 CLIENT_RPC_URL
if len(rpcUrl) == 0 {
return ConnInfo{ // 仅当为空时才回退到本地地址
Host: rpcCallAddr(cfg),
Scheme: GRPC,
}
}
parsedUrl, _ := url.Parse(rpcUrl)
return ConnInfo{
Host: parsedUrl.Host,
Scheme: Scheme(parsedUrl.Scheme),
}
}
cmd/frpp/shared/server.go 中的 runServer() 和 runClient() 都调用 NewMasterCli() → GetRPCConnInfo(),因此都使用了外部 URL。
修复方案
在 cmd/frpp/shared/providers.go 的 NewDefaultServerConfig() 中清空 Client.RPCUrl 和 Client.APIUrl,使内部 server 使用本地地址:
func NewDefaultServerConfig(ctx *app.Context) conf.Config {
// ... 已有代码 ...
tmpCfg := appInstance.GetConfig()
tmpCfg.Client.ID = defaultServer.ServerID
tmpCfg.Client.Secret = defaultServer.ConnectSecret
// 修复:内部 server 必须使用本地 RPC/API,而非外部 CLIENT_RPC_URL/CLIENT_API_URL
tmpCfg.Client.RPCUrl = ""
tmpCfg.Client.APIUrl = ""
appInstance.SetConfig(tmpCfg)
return tmpCfg
}
此修复有效的理由:
NewDefaultServerConfig 仅在 master 命令中使用,独立的 client 和 server 不受影响
- 当
Client.RPCUrl 为空时,GetRPCConnInfo() 回退到 rpcCallAddr(cfg) = MASTER_RPC_HOST:MASTER_RPC_PORT,使用 grpc:// 协议
GetPlatformInfo()(生成外部客户端配置的 API)从环境变量独立读取 Client.RPCUrl,不受此修改影响
相关 Issue
临时解决方案(不改代码)
不要设置 CLIENT_RPC_URL 和 CLIENT_API_URL,客户端直连:
--api-url http://YOUR_SERVER_IP:9005 --rpc-url grpc://YOUR_SERVER_IP:9004
Bug 描述
当设置
CLIENT_RPC_URL环境变量(用于反向代理场景,如wss://domain:443)时,master 内部的 server 和 client 组件错误地使用该外部 URL 连接自身,导致websocket: bad handshake错误,服务无法启动。环境
network_mode: hostgrpc_pass grpcs://127.0.0.1:9004复现步骤
期望行为
master 内部的 server/client 组件应直接连接本地 RPC 端口(
MASTER_RPC_HOST:MASTER_RPC_PORT),CLIENT_RPC_URL仅用于生成外部客户端的连接配置。实际行为
日志输出:
内部 server 组件尝试通过
wss://rpc.example.com:443连接,nginx 将请求转发到本地 gRPC 端口,但本地端口只接受原生 gRPC,不接受 WebSocket,导致握手失败。容器陷入无限重启。根因分析
conf/helper.go中的GetRPCConnInfo()始终优先使用cfg.Client.RPCUrl(来自CLIENT_RPC_URL环境变量),而非本地MASTER_RPC_HOST:MASTER_RPC_PORT:cmd/frpp/shared/server.go中的runServer()和runClient()都调用NewMasterCli()→GetRPCConnInfo(),因此都使用了外部 URL。修复方案
在
cmd/frpp/shared/providers.go的NewDefaultServerConfig()中清空Client.RPCUrl和Client.APIUrl,使内部 server 使用本地地址:此修复有效的理由:
NewDefaultServerConfig仅在master命令中使用,独立的client和server不受影响Client.RPCUrl为空时,GetRPCConnInfo()回退到rpcCallAddr(cfg)=MASTER_RPC_HOST:MASTER_RPC_PORT,使用grpc://协议GetPlatformInfo()(生成外部客户端配置的 API)从环境变量独立读取Client.RPCUrl,不受此修改影响相关 Issue
临时解决方案(不改代码)
不要设置
CLIENT_RPC_URL和CLIENT_API_URL,客户端直连: