fatedier/frp · critical

create control crypto read writer

Error message

create control crypto read writer: %w

What it means

During the frpc-frps control connection login, after authentication succeeds the client upgrades the raw TCP conn to an encrypted read/writer via newControlReadWriter using the negotiated crypto context. This error wraps any failure of that crypto setup, whose real causes live in pkg/proto/wire: un-JSON-able hello transcripts, an unknown AEAD algorithm selected by the server, an algorithm the client never advertised, or a wrong-length server random.

Solutions

  1. Run matching frpc and frps versions (same release)
  2. Ensure nothing between client and server terminates or rewrites the control connection (no TLS-offloading proxies on the frps port)
  3. Capture the wrapped cause — 'unknown selected crypto algorithm' vs 'decode ... transcript' points to server build vs payload corruption
  4. Retry on a clean network path to rule out middlebox interference
Defensive patterns

Strategy: try-catch

Try / catch

_, err := svc.Login(ctx)
if err != nil && strings.Contains(err.Error(), "create control crypto read writer") {
	// protocol/crypto mismatch with server: check version parity and network path
}

Prevention

When it happens

Trigger: Login to a server whose ServerHello selects a crypto algorithm the client did not offer, or that sends malformed hello payloads; effectively only with a mismatched/modified frps build or a man-in-the-middle altering the handshake. Skipped entirely for ssh-tunnel client spec connections.

Common situations: frpc and frps built from different forks/versions with divergent crypto negotiation; a transparent proxy (corporate firewall, TLS-terminating LB) corrupting the binary handshake; extremely rare protocol-level incompatibility after a downgrade.

Related errors


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

Appendix: source

Thrown at client/control_session.go:90

	loginMsg, err := d.buildLoginMsg(previousRunID)
	if err != nil {
		return nil, err
	}

	loginResult, err := d.exchangeLogin(conn, loginMsg)
	if err != nil {
		return nil, err
	}
	loginRespMsg := loginResult.resp
	if loginRespMsg.Error != "" {
		return nil, errors.New(loginRespMsg.Error)
	}

	var controlRW io.ReadWriter = conn
	if d.clientSpec == nil || d.clientSpec.Type != "ssh-tunnel" {
		controlRW, err = d.newControlReadWriter(conn, loginResult.crypto)
		if err != nil {
			return nil, fmt.Errorf("create control crypto read writer: %w", err)
		}
	}

	success = true
	return &SessionContext{
		Common:         d.common,
		RunID:          loginRespMsg.RunID,
		Conn:           msg.NewConn(conn, msg.NewReadWriter(controlRW, d.common.Transport.WireProtocol)),
		Auth:           d.auth,
		Connector:      newMessageConnector(connector, d.common.Transport.WireProtocol),
		VnetController: d.vnetController,
		UDPPacketCodec: loginResult.udpPacketCodec,
	}, nil
}

func (d *controlSessionDialer) buildLoginMsg(previousRunID string) (*msg.Login, error) {
	hostname, _ := os.Hostname()
	loginMsg := &msg.Login{

View on GitHub (pinned to 6c8a8d0a97)