fatedier/frp · error

natHoleRespMsg get error info: %s

Error message

natHoleRespMsg get error info: %s

What it means

The NatHoleResp message arrived intact but carries a non-empty Error field: the frps server explicitly rejected the hole-punch transaction. The message string is the server's own error text (e.g. transaction not found, no counterpart, analysis failed), so it is the authoritative reason for the refusal.

Source

Thrown at pkg/nathole/nathole.go:183

	ctx context.Context, transporter transport.MessageTransporter,
	laneKey string, m msg.Message, timeout time.Duration,
) (*msg.NatHoleResp, error) {
	timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	var natHoleRespMsg *msg.NatHoleResp
	m, err := transporter.Do(timeoutCtx, m, laneKey, msg.TypeNameNatHoleResp)
	if err != nil {
		return nil, fmt.Errorf("get natHoleRespMsg error: %v", err)
	}
	mm, ok := m.(*msg.NatHoleResp)
	if !ok {
		return nil, fmt.Errorf("get natHoleRespMsg error: invalid message type")
	}
	natHoleRespMsg = mm

	if natHoleRespMsg.Error != "" {
		return nil, fmt.Errorf("natHoleRespMsg get error info: %s", natHoleRespMsg.Error)
	}
	if len(natHoleRespMsg.CandidateAddrs) == 0 {
		return nil, fmt.Errorf("natHoleRespMsg get empty candidate addresses")
	}
	return natHoleRespMsg, nil
}

// MakeHole is used to make a NAT hole between client and visitor.
func MakeHole(ctx context.Context, listenConn *net.UDPConn, m *msg.NatHoleResp, key []byte) (*net.UDPConn, *net.UDPAddr, error) {
	xl := xlog.FromContextSafe(ctx)
	transactionID := NewTransactionID()
	sendToRangePortsFunc := func(conn *net.UDPConn, addr string) error {
		return sendSidMessage(ctx, conn, m.Sid, transactionID, addr, key, m.DetectBehavior.TTL)
	}

	listenConns := []*net.UDPConn{listenConn}
	var detectAddrs []string
	if m.DetectBehavior.Role == DetectRoleSender {

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Read the embedded server error text — it names the exact server-side refusal reason
  2. Verify the sk is running and the target proxy is registered before the visitor connects
  3. Retry the visit: transient analysis failures (race between client and visitor prepare messages) often succeed on a second attempt
  4. If both endpoints are behind symmetric NAT, configure a fallback (STUN server config or direct relay via server)
Defensive patterns

Strategy: try-catch

Try / catch

resp, err := nathole.ExchangeInfo(ctx, tp, laneKey, m, timeout)
if err != nil && strings.Contains(err.Error(), "natHoleRespMsg get error info") {
    // server-side refusal; err text after ': ' is the server's reason
    return fmt.Errorf("punch refused by server: %w", err)
}

Prevention

When it happens

Trigger: ExchangeInfo receives NatHoleResp with Error != "" — frps could not match client and visitor, the transaction ID expired, or the server-side analysis concluded hole punching is impossible.

Common situations: Visitor trying to reach an offline or not-yet-registered client; both peers behind symmetric NATs so the server analysis gives up; transaction timeout because client and visitor did not send their NatHolePrepare within the server's window.

Related errors


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