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

  1. Align frpc and frps versions so both support the same wire protocol set
  2. Set transport.wireProtocol to an empty string (defaults to v1) or exactly "v1"/"v2" in the client config
  3. If you drive RegisterControl programmatically, pass only wire.ProtocolV1 or wire.ProtocolV2 constants instead of string literals
  4. 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

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


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