fatedier/frp · error

get natHoleRespMsg error: invalid message type

Error message

get natHoleRespMsg error: invalid message type

What it means

Returned by nathole.PreCheck (pkg/nathole/nathole.go) when transporter.Do returns a message that is not *msg.NatHoleResp even though the request registered msg.TypeNameNatHoleResp as the expected response. The transport is supposed to correlate replies by transaction and expected type; a different concrete type indicates a protocol or transport-layer inconsistency rather than a user config problem.

Source

Thrown at pkg/nathole/nathole.go:107

	ctx context.Context, transporter transport.MessageTransporter,
	proxyName string, timeout time.Duration,
) error {
	timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	var natHoleRespMsg *msg.NatHoleResp
	transactionID := NewTransactionID()
	m, err := transporter.Do(timeoutCtx, &msg.NatHoleVisitor{
		TransactionID: transactionID,
		ProxyName:     proxyName,
		PreCheck:      true,
	}, transactionID, msg.TypeNameNatHoleResp)
	if err != nil {
		return fmt.Errorf("get natHoleRespMsg error: %v", err)
	}
	mm, ok := m.(*msg.NatHoleResp)
	if !ok {
		return fmt.Errorf("get natHoleRespMsg error: invalid message type")
	}
	natHoleRespMsg = mm

	if natHoleRespMsg.Error != "" {
		return fmt.Errorf("%s", natHoleRespMsg.Error)
	}
	return nil
}

// Prepare is used to do some preparation work before penetration.
func Prepare(stunServers []string, opts PrepareOptions) (*PrepareResult, error) {
	// discover for Nat type
	addrs, localAddr, err := Discover(stunServers, "")
	if err != nil {
		return nil, fmt.Errorf("discover error: %v", err)
	}
	if len(addrs) < 2 {
		return nil, fmt.Errorf("discover error: not enough addresses")

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Use the stock transport implementation provided by frp rather than a custom MessageTransporter
  2. Align frpc and frps versions
  3. In mock/test transporters, make Do return exactly *msg.NatHoleResp for NatHoleVisitor requests
  4. Log the concrete type returned by Do to see what actually arrived

Example fix

// before (mock transporter returning wrong type)
func (m *mockTP) Do(...) (msg.Message, error) { return &msg.Pong{}, nil }

// after
func (m *mockTP) Do(...) (msg.Message, error) { return &msg.NatHoleResp{}, nil }
Defensive patterns

Strategy: type-guard

Type guard

func isNatHoleResp(m msg.Message) bool {
    _, ok := m.(*msg.NatHoleResp)
    return ok
}

Try / catch

m, err := tp.Do(ctx, req, tid, msg.TypeNameNatHoleResp)
if err != nil {
    return err
}
if !isNatHoleResp(m) {
    // transport delivered an unexpected type: treat as protocol violation, reset transport
}

Prevention

When it happens

Trigger: nathole.PreCheck where the message dispatcher delivers an unexpected type for the transaction — e.g. a custom MessageTransporter implementation with lax type matching, a version-skewed peer sending a different response shape, or a deserialization path that decodes into the wrong struct.

Common situations: Custom transporters that do not honor the expected-response-type argument; frpc/frps version mismatch changing response encoding; tests with mock transporters returning generic messages.

Related errors


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