Background & Problem
Tailscale clients establish their coordination control plane session (TS2021) by making an HTTP POST /ts2021 request with non-standard upgrade
headers:
POST /ts2021 HTTP/1.1
Upgrade: tailscale-control-protocol
Connection: upgrade
X-Tailscale-Handshake: <base64>
When self-hosting a coordination server (e.g. Headscale) behind CDNs or restrictive reverse proxies (such as Cloudflare Tunnel / Cloudflare Proxy):
- RFC 6455 Violation: RFC 6455 Section 4.1 (https://datatracker.ietf.org/doc/html/rfc6455#section-4.1) mandates that the WebSocket upgrade
handshake must use the GET method.
- Upgrade Header Filtering: Cloudflare and similar edge proxies strictly filter WebSocket upgrade requests. They drop non-GET upgrades and only
accept standard Upgrade: websocket. Any non-standard Upgrade: tailscale-control-protocol sent with POST is immediately rejected with an HTTP
error (e.g. 500 Internal Server Error or 405 Method Not Allowed).
This limitation is well documented across the community:
Tailscale's repository actually already contains native WebSocket tunneling implementations (control/controlhttp/client_js.go, net/wsconn, and
controlhttpserver.acceptWebsocket), but it is only compiled in for js/wasm targets.
Proposed Solution
Add a control_websocket boolean option to TailscaleEndpointOptions:
{
"type": "tailscale",
"tag": "ts-ep",
"control_url": "https://headscale.example.com",
"control_websocket": true
}
Reference Implementation
A fully verified and tested implementation is available on my fork:
Background & Problem
Tailscale clients establish their coordination control plane session (TS2021) by making an HTTP
POST /ts2021request with non-standard upgradeheaders:
When self-hosting a coordination server (e.g. Headscale) behind CDNs or restrictive reverse proxies (such as Cloudflare Tunnel / Cloudflare Proxy):
handshake must use the GET method.
accept standard Upgrade: websocket. Any non-standard Upgrade: tailscale-control-protocol sent with POST is immediately rejected with an HTTP
error (e.g. 500 Internal Server Error or 405 Method Not Allowed).
This limitation is well documented across the community:
that Cloudflare Tunnel/Proxy is unsupported due to Tailscale's POST upgrade requirement.
Tailscale's repository actually already contains native WebSocket tunneling implementations (control/controlhttp/client_js.go, net/wsconn, and
controlhttpserver.acceptWebsocket), but it is only compiled in for js/wasm targets.
Proposed Solution
Add a control_websocket boolean option to TailscaleEndpointOptions:
{ "type": "tailscale", "tag": "ts-ep", "control_url": "https://headscale.example.com", "control_websocket": true }Reference Implementation
A fully verified and tested implementation is available on my fork: