fatedier/frp · error

missing v2 crypto negotiation

Error message

missing v2 crypto negotiation

What it means

Internal invariant error in newControlReadWriter (client/control_session.go:214): the client configured transport.wireProtocol = v2, so the control connection must be wrapped with AEAD crypto derived from the hello exchange, but the cryptoContext passed in is nil. In the current flow cryptoContext is only set when the v2 branch of exchangeLogin completed the ServerHello exchange, so nil here means v2 was requested yet negotiation never produced a context.

Source

Thrown at client/control_session.go:214

		}
		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
}

func (d *controlSessionDialer) newControlReadWriter(conn net.Conn, cryptoContext *wire.CryptoContext) (io.ReadWriter, error) {
	if d.common.Transport.WireProtocol == wire.ProtocolV2 {
		if cryptoContext == nil {
			return nil, errors.New("missing v2 crypto negotiation")
		}
		return netpkg.NewAEADCryptoReadWriter(
			conn,
			d.auth.EncryptionKey(),
			netpkg.AEADCryptoRoleClient,
			cryptoContext.Algorithm,
			cryptoContext.TranscriptHash,
		)
	}
	return netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey())
}

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Ensure frpc and frps versions match exactly when using wireProtocol v2
  2. Remove or correct transport.wireProtocol config so client and server agree on the protocol
  3. If you maintain a fork, guarantee exchangeLogin's v2 branch always yields a cryptoContext before newControlReadWriter is called with ProtocolV2
  4. Report with logs if stock binaries on identical versions hit this — it indicates a negotiation bug

Example fix

// before (fork/code path)
rw, err := d.newControlReadWriter(conn, nil) // v2 configured -> missing v2 crypto negotiation

// after
if d.common.Transport.WireProtocol == wire.ProtocolV2 && cryptoContext == nil {
    return nil, errors.New("v2 negotiated no crypto context; check server wireProtocol support")
}
rw, err := d.newControlReadWriter(conn, cryptoContext)
Defensive patterns

Strategy: validation

Validate before calling

if d.common.Transport.WireProtocol == wire.ProtocolV2 {
    // verify negotiation actually happened before building the RW
    if result.crypto == nil {
        return nil, errors.New("v2 configured but server did not negotiate; align versions or unset wireProtocol")
    }
}

Try / catch

rw, err := d.newControlReadWriter(conn, result.crypto)
if err != nil && strings.Contains(err.Error(), "missing v2 crypto negotiation") {
    // invariant break: v2 requested, no context — reconnect with v1 after config fix
    return reconnectWithV1()
}

Prevention

When it happens

Trigger: WireProtocol == ProtocolV2 while the code path that reads ServerHello and calls NewClientCryptoContext did not run or returned without setting the context — effectively a protocol-state mismatch between the configured wire protocol and what exchangeLogin actually negotiated.

Common situations: Version skew or a code change that makes the v2 hello conditional (e.g. server downgraded mid-handshake) while local config still forces v2; custom builds patching exchangeLogin; not something a config typo alone usually triggers.

Related errors


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