tailscale/tailscale · error

peerAPI dial requires tcp; %q not supported

Error message

peerAPI dial requires tcp; %q not supported

What it means

Returned by Dialer.dialPeerAPI in tailscale.com/net/tsdial when the network argument is not a TCP flavor. The Tailscale peerapi is an HTTP service reachable only over TCP, so the dialer accepts exactly "tcp", "tcp4", or "tcp6" and rejects everything else before attempting a connection. This is a programmer-error guard at the API boundary, not an environmental failure.

Source

Thrown at net/tsdial/tsdial.go:677

	}
	if version.IsMacGUIVariant() && tsaddr.IsTailscaleIP(ipp.Addr()) {
		return ipp, true, nil
	}
	return ipp, false, nil
}

// dialPeerAPI connects to a Tailscale peer's peerapi over TCP.
//
// network must a "tcp" type, and addr must be an ip:port. Name resolution
// is not supported.
func (d *Dialer) dialPeerAPI(ctx context.Context, network, addr string) (net.Conn, error) {
	if !buildfeatures.HasPeerAPIClient {
		return nil, feature.ErrUnavailable
	}
	switch network {
	case "tcp", "tcp6", "tcp4":
	default:
		return nil, fmt.Errorf("peerAPI dial requires tcp; %q not supported", network)
	}
	ipp, err := netip.ParseAddrPort(addr)
	if err != nil {
		return nil, fmt.Errorf("peerAPI dial requires ip:port, not name resolution: %w", err)
	}
	if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.Addr()) {
		if d.NetstackDialTCP == nil {
			return nil, errors.New("Dialer not initialized correctly")
		}
		return d.NetstackDialTCP(ctx, ipp)
	}
	return d.getPeerDialer().DialContext(ctx, network, addr)
}

// getPeerDialer returns the *net.Dialer to use to dial peers (e.g. for peerapi,
// "tailscale nc", or querying internal DNS servers over Tailscale)
//
// This is not used in netstack mode.

View on GitHub (pinned to 57c3357fdb)

Solutions

  1. Pass "tcp" (or "tcp4"/"tcp6") as the network argument when dialing peerapi
  2. If the network string comes from config or user input, validate it against a fixed allowlist before dialing
  3. For non-TCP peer communication use a different mechanism — peerapi has no UDP dial path

Example fix

// before
conn, err := d.dialPeerAPI(ctx, "udp", "100.101.102.103:12345")

// after
conn, err := d.dialPeerAPI(ctx, "tcp", "100.101.102.103:12345")
Defensive patterns

Strategy: validation

Validate before calling

func isPeerAPINetwork(network string) bool {
	switch network {
	case "tcp", "tcp4", "tcp6":
		return true
	default:
		return false
	}
}

if !isPeerAPINetwork(network) {
	return fmt.Errorf("refusing to dial peerapi with network %q", network)
}
conn, err := d.dialPeerAPI(ctx, network, addr)

Prevention

When it happens

Trigger: Calling dialPeerAPI (directly or via a peerapi client dial path) with network set to "udp", "unix", an empty string, or any value other than "tcp"/"tcp4"/"tcp6".

Common situations: Generic dial wrappers that forward a user- or config-supplied network string; porting UDP-based code to peerapi; copy-pasting a net.Dial call and forgetting to change the network argument.

Related errors


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