fatedier/frp · error
unsupported wire protocol: %s
Error message
unsupported wire protocol: %s
What it means
Returned by Service.RegisterControl when the wire protocol string carried on the client's control connection is neither wire.ProtocolV1 nor wire.ProtocolV2. The server switches on the negotiated wire protocol before it even creates a controller, so an unknown value aborts the login immediately. It exists to reject clients that send a protocol version this frps build does not understand.
Source
Thrown at server/service.go:784
func (svr *Service) RegisterControl(
ctlConn *msg.Conn,
loginMsg *msg.Login,
internal bool,
wireProtocol string,
udpPacketCodec string,
) (*Control, error) {
switch wireProtocol {
case wire.ProtocolV1:
if udpPacketCodec != "" {
return nil, fmt.Errorf("UDP packet codec %q requires wire protocol v2", udpPacketCodec)
}
case wire.ProtocolV2:
if udpPacketCodec != "" && udpPacketCodec != wire.UDPPacketCodecBinary {
return nil, fmt.Errorf("unsupported UDP packet codec selection: %s", udpPacketCodec)
}
default:
return nil, fmt.Errorf("unsupported wire protocol: %s", wireProtocol)
}
// If client's RunID is empty, it's a new client, we just create a new controller.
// Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one.
var err error
if loginMsg.RunID == "" {
loginMsg.RunID, err = util.RandID()
if err != nil {
return nil, err
}
}
if err := validation.ValidateRunID(loginMsg.RunID); err != nil {
return nil, fmt.Errorf("invalid run id: %w", err)
}
ctx := netpkg.NewContextFromConn(ctlConn)
xl := xlog.FromContextSafe(ctx)
xl.AppendPrefix(loginMsg.RunID)
ctx = xlog.NewContext(ctx, xl)View on GitHub (pinned to 6c8a8d0a97)
Solutions
- Align frpc and frps versions so both support the same wire protocol set
- Set transport.wireProtocol to an empty string (defaults to v1) or exactly "v1"/"v2" in the client config
- If you drive RegisterControl programmatically, pass only wire.ProtocolV1 or wire.ProtocolV2 constants instead of string literals
- Check server logs for the exact offending value in the message to identify which peer sent it
Example fix
# frpc.toml — before transport.wireProtocol = "v3" # after transport.wireProtocol = "v2"
Defensive patterns
Strategy: validation
Validate before calling
import "github.com/fatedier/frp/pkg/wire"
func validWireProtocol(p string) bool {
return p == "" || p == wire.ProtocolV1 || p == wire.ProtocolV2
}
// client side: if !validWireProtocol(cfg.Transport.WireProtocol) { fix config before dialing } Prevention
- Pin frpc and frps to compatible versions so the wire protocol vocabulary matches
- Never hand-type protocol strings; import the wire package constants
- Fail fast on config load if wireProtocol is not one of the known constants
When it happens
Trigger: Calling RegisterControl (or logging in from an frpc) with a wireProtocol argument other than the exact strings "v1"/"v2" (e.g. "v3", "", "V2" with wrong casing). Typically happens when a custom client hardcodes a protocol name, when a ClientHello negotiates an unknown value, or when mixing a newer frpc with an older frps that lacks that protocol constant.
Common situations: Version skew between frpc and frps (client newer than server and advertising a protocol the server predates); hand-rolled clients or tests that pass an arbitrary protocol string; typo in transport.wireProtocol config; proxy/load balancer mangling the handshake.
Related errors
- serverHello.Error
- create control crypto read writer: %w
- unexpected frame type %d, want %d
- invalid transport.wireProtocol, optional values are %v
- missing v2 crypto negotiation
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/822ef7aabbcbdd68.
Report an issue: GitHub.