tailscale/tailscale · warning

parse query parameters: %v

Error message

parse query parameters: %v

What it means

Server-side: r.ParseForm failed while extracting the handshake parameter from the WebSocket request's query string. The query component is malformed (invalid percent-escapes, stray characters), which net/url rejects. Essentially only hostile or badly broken clients produce this.

Source

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

		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)
	}

	// Do not bind the conn's lifetime to ctx: it's typically a request
	// context that net/http cancels once the calling handler returns, and
	// the conn may be served beyond that (tailscale/corp#46806). The
	// handshake below is still bounded by any ctx deadline, which
	// controlbase.Server applies to the conn directly.
	conn := wsconn.NetConn(context.WithoutCancel(ctx), c, websocket.MessageBinary, r.RemoteAddr)

View on GitHub (pinned to 0fd2f14deb)

Solutions

  1. Build query strings with url.Values.Encode() instead of string concatenation
  2. Log the raw query on failure to identify the offending client
  3. Treat as a bad request: the socket is already closed with 1008 by the server

Example fix

// before
u := fmt.Sprintf("wss://ctrl/ts2021?X-Tailscale-Handshake=%s&ts=%d", initB64, time.Now().Unix())

// after
q := url.Values{}
q.Set(controlhttpcommon.HandshakeHeaderName, initB64)
u := "wss://ctrl/ts2021?" + q.Encode()
Defensive patterns

Strategy: try-catch

Validate before calling

// Client-side: build the query with url.Values so it always parses server-side
q := url.Values{controlhttpcommon.HandshakeHeaderName: []string{initB64}}
u := endpoint + "?" + q.Encode()

Try / catch

if err := r.ParseForm(); err != nil {
    // Socket already closed with 1008; just log and drop the request
    log.Printf("malformed query from %s: %v", r.RemoteAddr, err)
    return
}

Prevention

When it happens

Trigger: Query strings containing invalid escapes like '%zz', control characters, or truncated URLs; fuzzers and scanners sending malformed URLs to the control endpoint.

Common situations: Hand-built URL strings without proper encoding, fuzz testing of the websocket endpoint, upstream bugs concatenating query params unsafely.

Related errors


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