tailscale/tailscale · error

failed to receive: %w

Error message

failed to receive: %w

What it means

In the TUN bandwidth probe, a goroutine loops on toc.Recv() to pull packets relayed back over the 'to' DERP connection and inject them into the TUN device. This error fires when Recv itself fails — the receiving DERP stream broke mid-transfer (the TUN-mode analogue of the pair probe's receive error).

Source

Thrown at prober/derp.go:1015

	// This goroutine reads packets from the `toc` DERP client and writes them towards the TUN.
	// It only reports errors to `recvErrC` channel.
	wg.Add(1)
	recvErrC := make(chan error, 1)
	go func() {
		defer wg.Done()

		// Depending on platform, we need some space for headers at the front
		// of TUN I/O op buffers. The below constant is more than enough space
		// for any platform that this might run on.
		tunWriteStartOffset := device.MessageTransportHeaderSize
		buf := make([]byte, mtu+tunWriteStartOffset)
		bufs := make([][]byte, 1)

		fromDERPPubKey := fromc.SelfPublicKey()
		for {
			m, err := toc.Recv()
			if err != nil {
				recvErrC <- fmt.Errorf("failed to receive: %w", err)
				return
			}
			switch v := m.(type) {
			case derp.ReceivedPacket:
				if v.Source != fromDERPPubKey {
					recvErrC <- fmt.Errorf("got data packet from unexpected source, %v", v.Source)
					return
				}
				pkt := v.Data
				copy(buf[tunWriteStartOffset:], pkt)
				bufs[0] = buf[:len(pkt)+tunWriteStartOffset]
				if _, err := dev.Write(bufs, tunWriteStartOffset); err != nil {
					recvErrC <- fmt.Errorf("failed to write to TUN device: %w", err)
					return
				}
			case derp.KeepAliveMessage:
				// Silently ignore.
			default:

View on GitHub (pinned to 5201273aec)

Solutions

  1. Inspect the wrapped error for EOF/ECONNRESET/TLS causes to distinguish a drop from a protocol failure.
  2. Re-run with a smaller transfer size: if small transfers pass, an intermediary idle timeout is truncating long streams.
  3. Check the 'to' derper's logs and connection limits for the failure window.
  4. For probes behind proxies, raise idle timeouts or exclude DERP traffic from the proxy.
Defensive patterns

Strategy: retry

Validate before calling

if err := toc.Connect(ctx); err != nil {
    return fmt.Errorf("to client not connected: %w", err)
}

Type guard

func isConnDropped(err error) bool {
    return errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) ||
        errors.Is(err, syscall.ECONNRESET)
}

Try / catch

Classify the wrapped cause: EOF/ECONNRESET -> drop, recreate clients and retry once; TLS error -> investigate certs; persistent for long transfers only -> shrink size or raise intermediary idle timeouts instead of retrying.

Prevention

When it happens

Trigger: toc.Recv() returns an error: the derper closed or reset the client connection, a TLS stream failure, a protocol error frame, or the client was closed locally by the probe teardown path.

Common situations: Receiving derper restarting during a long bandwidth transfer; LB/proxy idle or request timeouts cutting the long-lived HTTPS stream; server evicting the client; very large transfer sizes stretching the connection past intermediary limits.

Related errors


AI-assisted analysis of tailscale/tailscale@5201273aec (2026-08-18). Data as JSON: /api/errors/fdac74407d1caca8. Report an issue: GitHub.