tailscale/tailscale · error

Could not accept WebSocket connection %v

Error message

Could not accept WebSocket connection %v

What it means

Server-side: nhooyr/websocket's Accept rejected the WebSocket upgrade request (malformed or missing Sec-WebSocket-Key/Version, wrong method, header violations). Note the underlying error is attached with %v, not %w, so errors.Is/As will not unwrap it. Compression is intentionally disabled because Noise traffic is incompressible and Safari's compression is broken.

Source

Thrown at control/controlhttp/controlhttpserver/controlhttpserver.go:138

	return nc, nil
}

// acceptWebsocket upgrades a WebSocket connection (from a client that cannot
// speak HTTP) to a Tailscale control protocol base transport connection.
func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {
	c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
		Subprotocols:   []string{controlhttpcommon.UpgradeHeaderValue},
		OriginPatterns: []string{"*"},
		// Disable compression because we transmit Noise messages that are not
		// compressible.
		// Additionally, Safari has a broken implementation of compression
		// (see https://github.com/nhooyr/websocket/issues/218) that makes
		// enabling it actively harmful.
		CompressionMode: websocket.CompressionDisabled,
	})
	if err != nil {
		return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)
	}
	if c.Subprotocol() != controlhttpcommon.UpgradeHeaderValue {
		c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol")
		return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol())
	}
	if err := r.ParseForm(); err != nil {
		c.Close(websocket.StatusPolicyViolation, "Could not parse parameters")
		return nil, fmt.Errorf("parse query parameters: %v", err)
	}
	initB64 := r.Form.Get(controlhttpcommon.HandshakeHeaderName)
	if initB64 == "" {
		c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter")
		return nil, errors.New("no tailscale handshake parameter in HTTP request")
	}
	init, err := base64.StdEncoding.DecodeString(initB64)
	if err != nil {
		c.Close(websocket.StatusPolicyViolation, "invalid tailscale handshake parameter")
		return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)

View on GitHub (pinned to 0fd2f14deb)

Solutions

  1. Use a real WebSocket client library (coder/websocket, gorilla/websocket) instead of raw HTTP
  2. Ensure intermediaries forward the Sec-* headers unmodified
  3. Match the server's expectations: GET request, HTTP/1.1, valid Sec-WebSocket-Key

Example fix

// before
resp, _ := http.Get("wss://ctrl/ts2021") // no websocket handshake -> Accept fails

// after
ctx := context.Background()
c, _, err := websocket.Dial(ctx, "wss://ctrl/ts2021", &websocket.DialOptions{
    Subprotocols: []string{"tailscale-control-protocol"},
    CompressionMode: websocket.CompressionDisabled,
})
Defensive patterns

Strategy: try-catch

Validate before calling

// Client-side: verify you are speaking real WebSocket before hitting the server
if !strings.HasPrefix(url, "ws://") && !strings.HasPrefix(url, "wss://") {
    return errors.New("control websocket endpoint requires a ws:// or wss:// URL")
}

Try / catch

c, err := websocket.Accept(w, r, opts)
if err != nil {
    // Note: underlying cause is %v-formatted; match on message, errors.Is will not work
    log.Printf("bad websocket handshake from %s: %v", r.RemoteAddr, err)
    return // request already rejected with an HTTP error
}

Prevention

When it happens

Trigger: A plain HTTP request hitting the websocket path; proxies stripping WebSocket headers; clients not using a real WebSocket library; Origin patterns are '*' so origin is not the blocker — header validity is.

Common situations: Anti-virus/SSL inspectors breaking websocket handshakes, curl-based probes, hand-rolled clients, corporate proxies disabling websocket forwarding.

Related errors


AI-assisted analysis of tailscale/tailscale@0fd2f14deb (2026-08-18). Data as JSON: /api/errors/33fdf96e0da87445. Report an issue: GitHub.