Problem connecting to websocket with the client in a different origin than the server

I'm trying to make a simple web application (html+css+js) to monitor the status of servers of other applications that use zrok.

My servers that use zrok support connection upgrade to websocket. So the idea is that the monitor maintains the websocket connection to show the servers that are active or not. As in the image below.

I'm having problems connecting to the websocket using the zrok url when the websocket client has a different origin than the zrok url. When the client is in the same request origin, the connection works perfectly.

Initially I thought it was some browser restriction or CORS of my application on the server. But then I did a test using:
wss://echo.websocket.org and I was able to connect perfectly even with the client in a different origin, which seems to eliminate a browser restriction.

I also did another test using CURL with zrok's url but specifying some headers:

curl --include --no-buffer --header "Connection: Upgrade" --header "Upgrade: websocket" --header "Host: wfwljbgpjt5n.share.zrok.io" --header "Origin: https://wfwljbgpjt5n.share.zrok.io" --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" --header "Sec-WebSocket-Version: 13" https://wfwljbgpjt5n.share.zrok.io/

And it also seems to have worked, as I had the following return:

HTTP/1.1 101 Switching Protocols
Date: Tue, 10 Jun 2025 12:42:29 GMT
Connection: upgrade
Sec-Websocket-Accept: qGEgH3En71di5rrssAZTmtRTyFk=
Server: Pca 2.5.2.1
Upgrade: websocket

Warning: Binary output can mess up your terminal. Use "--output -" to tell curl to output it to your terminal anyway,
Warning: or consider "--output " to save to a file.

So the problem seems to be when you try to use javascript on a page with a different origin like:

const ws = new WebSocket(wss://wfwljbgpjt5n.share.zrok.io);

I also tried using the --insecure option on the "zrok share" command but had the same problem.

I'll also leave here the code I'm using to test:
Monitor.zip (2.0 KB)

Thanks!

I did some more tests, and it seems to be something related to the "host" header, when we use a native Javascript WebSocket instance like:

const ws = new WebSocket(wss://wfwljbgpjt5n.share.zrok.io);

We don't have parameters to inform custom headers, in this case the "Host" header will necessarily be the same as the HTML document opened in the browser.

When we use curl, we can customize or hide the request headers, and then I can get the websocket to respond under zrko, Ex:

This works (here I hid the Host):

curl --include --no-buffer --header "Connection: Upgrade" --header "Upgrade: websocket" --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" --header "Sec-WebSocket-Version: 13" wss://wfwljbgpjt5n.share.zrok.io/

But if I try to inform a Host header that is different from the Host of the zrok url like:

This does not work (here I set the Host = localhost:8080):

curl --include --no-buffer --header "Connection: Upgrade" --header "Upgrade: websocket" --header "Host: localhost:8080" --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" --header "Sec-WebSocket-Version: 13" wss://wfwljbgpjt5n.share.zrok.io/

I get the error:
curl: (22) Refused WebSockets upgrade: 200

If I leave the Host the same as the zrok url, it also works:

curl --include --no-buffer --header "Connection: Upgrade" --header "Upgrade: websocket" --header "Host: wfwljbgpjt5n.share.zrok.io" --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" --header "Sec-WebSocket-Version: 13" wss://wfwljbgpjt5n.share.zrok.io/

So it seems to me that there is some kind of validation that is requiring that the Host header must be the same on the zrok server side.

How can we solve this?

Hi @giorgiobazzo, welcome to the community and to zrok!

I believe you can run zrok in 'caddy' mode and rewrite the header but I haven't tried it myself. Have you had a look at that?

Hi @TheLumberjack ,

I don't know about caddy mode.

Today I run it using the command:

zrok share reserved wfwljbgpjt5n --override-endpoint http://localhost:9496

How do I use this mode?

Thanks!

Hi @TheLumberjack,

According to WebSocket

WebSockets are cross-origin by nature, but in my tests I see that this is not possible if we are using zrok.

I think zrok should make this possible, because when I test using other methods such as VPN, reverse proxy, or direct IP, the connection works.

From what I saw in the link you sent I would have to use a custom Self-Host for caddy mode, right? I really wouldn't like to have to do that.

Thanks!

Oh my bad, that is the wrong link, I was thinking about using --backend-mode=caddy as in zrok share public --backend-mode=caddy caddy.file.here. I think with that you'd be able to rewrite the host header example caddy file is here that you'd use: https://docs.zrok.io/simple_reverse_proxy.Caddyfile

I tried this approach, but I had the same problem. When the websocket request is created by the browser in a URL from another origin, or with origin = null (in the case of opening the HTML directly from the HD), this request does not even reach my backend.

When I use curl with the "Host" header, for example: --header "Host: wfwljbgpjt5n.share.zrok.io", I can make the request arrive. The problem is that the browser does not allow you to manipulate the websocket client headers like the curl command does.

This seems to be some kind of handling by the API used by the zrok command, which is not redirecting websocket connections with an origin other than the share created.

Another problem is that I cannot use --backend-mode=caddy with zrok share reserved.

It's not zrok, it's a problem with any proxied http response. Your client sends an http request to zrok and it needs to use zrok's host header. That's how zrok finds your service. It is proxied to your zrok share where the share creates a normal tcp connection to the target. zrok is sending your original http request untouched. That's the problem. At your share side, you need to rewrite the http request and replace the host header. I thought caddy could do this but I've never done it myself. This doc seems to indicate it's possible but like i said, I've never tried it myself. reverse_proxy (Caddyfile directive) — Caddy Documentation

I'll see if anyone has done what you're trying to do if that's not enough info. Cheers

There's an example of rewriting the Host header using the caddy backend here:

1 Like

Hello,

Today I'm trying this approach again, as I need a reserved address using the command:

zrok share reserved wfwljbgpjt5n --override-endpoint caddy.txt

according to: zrok/etc/caddy/README.md at main · openziti/zrok · GitHub

my caddy file is like this:

# global config must be first
{
    # no listen on 2019/tcp with admin API
    admin off
}

# zrok site block
http:// {
    # Bind to the zrok share
    bind {{ .ZrokBindAddress }}

    # All other traffic goes to 127.0.0.1:9496
    reverse_proxy /* 127.0.0.1:9496 {
        header_up Host wfwljbgpjt5n.share.zrok.io
        header_up X-Real-IP {http.request.header.x-forwarded-for}                                                          
    }
}

but sharing is not working, I'm getting the error:

ERROR zrok/endpoints/proxy.newReverseProxy.func2: error proxying: unsupported protocol scheme ""

zrok.exe version is v1.0.4 [3f5db643]

What am I doing wrong?

Thanks again!

PS:

--override-endpoint seems to ignore caddy.txt and doesn't load the settings

I just tried using:
zrok share public --backend-mode=caddy

But we had the same problems with websockets, even removing or rewriting the Host header through the caddy file.

When I remove or rewrite the Host header in the request using the CURL command, the websocket works.

So it seems to me that manipulating the Host header in the caddy file has no effect.

I ran it using --verbose and when the Host header is different from the share, no message appears in the console.

When I remove or rewrite the Host header in the request using the CURL command, the messages start to appear.