fatedier/frp · error

serverHello.Error

Error message

serverHello.Error

What it means

With transport.wireProtocol = v2, frpc sends a ClientHello frame and frps answers with a ServerHello frame; if that frame carries a non-empty Error string, the client aborts the connection with that message (client/control_session.go:191). This is a v2 protocol handshake rejection — the server understood the magic/frame layer but refused the session before crypto setup.

Source

Thrown at client/control_session.go:191

		_ = conn.SetReadDeadline(time.Time{})
	}()

	var cryptoContext *wire.CryptoContext
	var udpPacketCodec string
	if wireConn != nil {
		serverHelloFrame, err := wireConn.ReadFrame()
		if err != nil {
			return nil, err
		}
		if serverHelloFrame.Type != wire.FrameTypeServerHello {
			return nil, fmt.Errorf("unexpected frame type %d, want %d", serverHelloFrame.Type, wire.FrameTypeServerHello)
		}
		var serverHello wire.ServerHello
		if err := wireConn.UnmarshalFrame(serverHelloFrame, &serverHello); err != nil {
			return nil, err
		}
		if serverHello.Error != "" {
			return nil, errors.New(serverHello.Error)
		}
		cryptoContext, err = wire.NewClientCryptoContext(clientHelloPayload, serverHelloFrame.Payload)
		if err != nil {
			return nil, err
		}
		udpPacketCodec = serverHello.Selected.Message.UDPPacketCodec
	}

	var loginRespMsg msg.LoginResp
	if err := rw.ReadMsgInto(&loginRespMsg); err != nil {
		return nil, err
	}
	return &loginExchangeResult{
		resp:           &loginRespMsg,
		crypto:         cryptoContext,
		udpPacketCodec: udpPacketCodec,
	}, nil
}

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Align frpc and frps to the same version before using wireProtocol v2
  2. Read the embedded server message — it is frps's own refusal reason
  3. If v2 is optional in your setup, remove transport.wireProtocol=v2 to fall back to v1 and confirm connectivity
  4. Capture frps logs during the handshake for the matching rejection

Example fix

# before (frpc.toml)
transport:
  wireProtocol = "v2"   # server too old -> ServerHello.Error

# after
transport:
  wireProtocol = "v2"  # keep only after upgrading frps to the matching version
Defensive patterns

Strategy: try-catch

Try / catch

_, err := d.exchangeLogin(conn, loginMsg)
if err != nil {
    // ServerHello.Error is a definitive v2 handshake rejection
    if strings.Contains(err.Error(), "handshake") || wireProtoV2 {
        log.Error("v2 handshake rejected by frps; falling back to wireProtocol v1 requires restart with changed config")
    }
    return err
}

Prevention

When it happens

Trigger: wireProtocol v2 enabled on the client while the server rejects the negotiation: unsupported/unsupported-version v2 handshake, bootstrap options the server refuses (transport/TLS/TCPMux combination), or a protocol-level policy rejection communicated in ServerHello.Error.

Common situations: Client and server on different frp versions where the v2 handshake semantics changed; enabling v2 wire protocol against an frps build that validates hello contents strictly; middleboxes altering the stream so the server bails during hello.

Related errors


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/567abc861c3a6768. Report an issue: GitHub.