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
- Ensure frpc and frps versions match exactly when using wireProtocol v2
- Remove or correct transport.wireProtocol config so client and server agree on the protocol
- If you maintain a fork, guarantee exchangeLogin's v2 branch always yields a cryptoContext before newControlReadWriter is called with ProtocolV2
- 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
- Keep frpc/frps versions identical when wireProtocol v2 is on
- In forks, assert cryptoContext != nil immediately after exchangeLogin for v2
- Treat this error as a bug report candidate, not a config typo
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.