Replies: 2 comments
|
I reproduced this behavior. Replacing the direct However, this does not make the complete Settings UI available over a non-loopback HTTP origin. In my test, the next failures were:
A reverse proxy can technically normalize the upstream Host/Origin and make the client behave as loopback, and I verified that Therefore the safe workaround remains HTTPS plus authentication, or an SSH/platform port forward that preserves a localhost browser origin. The UUID fallback should still be fixed upstream independently. I posted the more detailed reproduction and security notes in #3302. |
|
Uh oh!
There was an error while loading. Please reload this page.
Summary
When the DSH web UI is served over plain HTTP on a non-loopback host (e.g.
http://<LAN-IP>:3080orhttp://<tailscale-IP>:3080), the browser treats the origin as a non-secure context (window.isSecureContext === false). In that environmentcrypto.randomUUIDisundefined.dsh-client-connection'sAbstractApiClient.mintRpcId()callscrypto.randomUUID()directly, so minting the very first RPC id (host.describe) throwsTypeError: crypto.randomUUID is not a function. That throw is swallowed by the connection loop'scatch, which aborts the current generation — closing bothevents.muxandevents.hostWebSockets while they are stillCONNECTING.Result: the browser logs
WebSocket connection to 'ws://…/api/events.mux' failed: WebSocket is closed before the connection is established., thehost.describerequest is never actually sent, and the whole UI (sidebar etc.) stays blank, stuck in an infinite reconnect loop.The same UI works perfectly when the same server is reached over HTTPS or over loopback (
http://127.0.0.1:3080), because those are secure contexts wherecrypto.randomUUIDexists.Environment
@deepseek-ai/dsh-*packages at 0.1.0-rc.6)http://100.68.55.117:3080(Tailscale IP of the host, plain HTTP) →tailscale serve --tcp 3080 3080→127.0.0.1:3080window.isSecureContext === false,typeof crypto.randomUUID === "undefined",typeof crypto.getRandomValues === "function"Steps to reproduce
dsh --profile <web-profile> --port 3080bound to loopback (the only supported--hosttoday).tailscale serve --tcp 3080 3080) or a plain-HTTP reverse proxy — so the browser origin ishttp://<non-loopback-host>:3080.http://<non-loopback-host>:3080in a browser.Observed: the sidebar is completely blank; DevTools shows the
events.mux/events.hostWebSockets failing with "closed before the connection is established", and[web-runtime] connection lost, retry #Nrepeats forever. ThePOST /api/host.describerequest is never issued.Expected vs actual
--trusted-hostmechanism for exactly this), minting RPC ids through the browser-saferandomUuid()helper.mintRpcId()usescrypto.randomUUID()(secure-context-only), so the first RPC id mint throws, the handshake aborts, and the UI never loads on non-secure contexts.Root cause (from reading the source)
In
@deepseek-ai/dsh-client-connection(lib/client.js):The same package already ships a browser-safe helper a few lines away:
…but
mintRpcId()doesn't use it. Thehost.describe()unary call (callUnary) reachesmintRpcId()before it issues the HTTP request, so the failure happens before any/apitraffic is sent — which is why the symptom is "WebSocket closed before established" (the abort closes the WebSockets) rather than an HTTP error.There is at least one more direct
crypto.randomUUID()usage in the same bundle (MessageId(crypto.randomUUID())for session messages), which hits the same wall on non-secure contexts.Suggestion
mintRpcId()(and any other client-side id minting) userandomUuid()instead ofcrypto.randomUUID(), sincerandomUuid()already exists and is documented as the insecure-origin-safe path; and/orrandomUuid()-backed polyfill forcrypto.randomUUIDon the client, so no call site can accidentally depend on the secure-context-only API.This is the same class of "works over loopback/HTTPS, breaks over plain HTTP on a non-loopback host" issue that's easy to miss in dev (where
127.0.0.1is always a secure context).All reactions