Skip to content

[BUG] CLIENT_RPC_URL 导致 master 内部组件通过反代连接自身,websocket 握手失败 #161

Description

@guiyinrenshi

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

复现步骤

  1. 使用以下 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
  1. 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;
    }
}
  1. 启动容器

期望行为

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.goNewDefaultServerConfig() 中清空 Client.RPCUrlClient.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
}

此修复有效的理由:

  1. NewDefaultServerConfig 仅在 master 命令中使用,独立的 clientserver 不受影响
  2. Client.RPCUrl 为空时,GetRPCConnInfo() 回退到 rpcCallAddr(cfg) = MASTER_RPC_HOST:MASTER_RPC_PORT,使用 grpc:// 协议
  3. GetPlatformInfo()(生成外部客户端配置的 API)从环境变量独立读取 Client.RPCUrl,不受此修改影响

相关 Issue

临时解决方案(不改代码)

不要设置 CLIENT_RPC_URLCLIENT_API_URL,客户端直连:

--api-url http://YOUR_SERVER_IP:9005 --rpc-url grpc://YOUR_SERVER_IP:9004

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions