fatedier/frp · error

wait detect message error

Error message

wait detect message error: %v

What it means

MakeHole failed because waitDetectMessage returned an error on the single listening UDP socket: the peer's detect packet could not be received before the deadline (ReadFromUDP error, deadline exceeded, or an invalid SID/role match). This is the actual hole-punch packet exchange failing after the coordination succeeded.

Solutions

  1. Verify both sides use the same auth token so the detect messages decrypt and the SID matches
  2. Retry the visit — hole punching is inherently racy and often succeeds on later attempts
  3. If it consistently fails, one endpoint is likely behind a symmetric NAT: fall back to relaying through frps (do not rely on P2P)
  4. Check DetectBehavior.ReadTimeoutMs in the server response; raise it server-side if the network is slow
Defensive patterns

Strategy: retry

Try / catch

conn, addr, err := nathole.MakeHole(ctx, l, resp, key)
if err != nil && strings.Contains(err.Error(), "wait detect message") {
    // UDP punch is racy: retry N times with backoff before giving up
    for i := 0; i < 3 && err != nil; i++ {
        time.Sleep(time.Duration(i+1) * 500 * time.Millisecond)
        conn, addr, err = retryMakeHole()
    }
}

Prevention

When it happens

Trigger: MakeHole with exactly one listenConn and waitDetectMessage errors: UDP read deadline expires, the socket errors, or every incoming packet is discarded because the decrypted SID does not match m.Sid for the expected role.

Common situations: NAT rewrites the source port so detect packets arrive from an unexpected address; symmetric NAT on either side blocks the hole; packet loss or heavy UDP filtering; wrong key (auth token) making SID decryption fail so valid packets are dropped.

Related errors


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

Appendix: source

Thrown at pkg/nathole/nathole.go:255

		}
	}
	if m.DetectBehavior.SendRandomPorts > 0 {
		ctx, cancel := context.WithCancel(ctx)
		defer cancel()
		for i := range listenConns {
			go sendSidMessageToRandomPorts(ctx, listenConns[i], m.CandidateAddrs, m.DetectBehavior.SendRandomPorts, sendToRangePortsFunc)
		}
	}

	timeout := 5 * time.Second
	if m.DetectBehavior.ReadTimeoutMs > 0 {
		timeout = time.Duration(m.DetectBehavior.ReadTimeoutMs) * time.Millisecond
	}

	if len(listenConns) == 1 {
		raddr, err := waitDetectMessage(ctx, listenConns[0], m.Sid, key, timeout, m.DetectBehavior.Role)
		if err != nil {
			return nil, nil, fmt.Errorf("wait detect message error: %v", err)
		}
		return listenConns[0], raddr, nil
	}

	type result struct {
		lConn *net.UDPConn
		raddr *net.UDPAddr
	}
	resultCh := make(chan result)
	for _, conn := range listenConns {
		go func(lConn *net.UDPConn) {
			addr, err := waitDetectMessage(ctx, lConn, m.Sid, key, timeout, m.DetectBehavior.Role)
			if err != nil {
				lConn.Close()
				return
			}
			select {
			case resultCh <- result{lConn: lConn, raddr: addr}:

View on GitHub (pinned to 6c8a8d0a97)