{"record":{"id":"f552a294a0e046a1","repo":"tailscale/tailscale","slug":"peerapi-dial-requires-tcp-q-not-supported","errorCode":null,"errorMessage":"peerAPI dial requires tcp; %q not supported","messagePattern":"peerAPI dial requires tcp; %q not supported","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"net/tsdial/tsdial.go","lineNumber":677,"sourceCode":"\t}\n\tif version.IsMacGUIVariant() && tsaddr.IsTailscaleIP(ipp.Addr()) {\n\t\treturn ipp, true, nil\n\t}\n\treturn ipp, false, nil\n}\n\n// dialPeerAPI connects to a Tailscale peer's peerapi over TCP.\n//\n// network must a \"tcp\" type, and addr must be an ip:port. Name resolution\n// is not supported.\nfunc (d *Dialer) dialPeerAPI(ctx context.Context, network, addr string) (net.Conn, error) {\n\tif !buildfeatures.HasPeerAPIClient {\n\t\treturn nil, feature.ErrUnavailable\n\t}\n\tswitch network {\n\tcase \"tcp\", \"tcp6\", \"tcp4\":\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"peerAPI dial requires tcp; %q not supported\", network)\n\t}\n\tipp, err := netip.ParseAddrPort(addr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"peerAPI dial requires ip:port, not name resolution: %w\", err)\n\t}\n\tif d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.Addr()) {\n\t\tif d.NetstackDialTCP == nil {\n\t\t\treturn nil, errors.New(\"Dialer not initialized correctly\")\n\t\t}\n\t\treturn d.NetstackDialTCP(ctx, ipp)\n\t}\n\treturn d.getPeerDialer().DialContext(ctx, network, addr)\n}\n\n// getPeerDialer returns the *net.Dialer to use to dial peers (e.g. for peerapi,\n// \"tailscale nc\", or querying internal DNS servers over Tailscale)\n//\n// This is not used in netstack mode.","sourceCodeStart":659,"sourceCodeEnd":695,"githubUrl":"https://github.com/tailscale/tailscale/blob/57c3357fdb542d26c6f9e9f0b815ae5077e63d77/net/tsdial/tsdial.go#L659-L695","documentation":"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.","triggerScenarios":"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\".","commonSituations":"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.","solutions":["Pass \"tcp\" (or \"tcp4\"/\"tcp6\") as the network argument when dialing peerapi","If the network string comes from config or user input, validate it against a fixed allowlist before dialing","For non-TCP peer communication use a different mechanism — peerapi has no UDP dial path"],"exampleFix":"// before\nconn, err := d.dialPeerAPI(ctx, \"udp\", \"100.101.102.103:12345\")\n\n// after\nconn, err := d.dialPeerAPI(ctx, \"tcp\", \"100.101.102.103:12345\")","handlingStrategy":"validation","validationCode":"func isPeerAPINetwork(network string) bool {\n\tswitch network {\n\tcase \"tcp\", \"tcp4\", \"tcp6\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nif !isPeerAPINetwork(network) {\n\treturn fmt.Errorf(\"refusing to dial peerapi with network %q\", network)\n}\nconn, err := d.dialPeerAPI(ctx, network, addr)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always dial peerapi with \"tcp\"","Whitelist network strings from config before forwarding them to any dialer","Fail fast on unknown network values instead of passing them through"],"tags":["tailscale","peerapi","dialer","network","go"],"backgroundTag":"unsupported-network-protocol","analyzedSha":"57c3357fdb542d26c6f9e9f0b815ae5077e63d77","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}