WebSocket connection upgrade refused behind Netbird Cloud reverse proxy

Describe the problem

I have a peer on Netbird Cloud that’s hosting a WebSocket server (Mastodon, if that matters). I’ve set up an HTTPS reverse proxy from a custom domain to the peer at port 80, with both “Pass Host Header” and “Rewrite Redirects” turned on. The peer is reachable at domain via HTTPS, but the WebSocket is not reachable.

To Reproduce

Steps to reproduce the behavior:

  1. Set up as described above
  2. Open Mastodon at <custom domain> in a web browser and open the web console
  3. Console shows errors saying it can’t establish a connection to the server at wss://<custom domain>/api/v1/streaming/

Expected behavior

Mastodon should run correctly and access the streaming WebSocket without issue.

Are you using NetBird Cloud?

Yes

NetBird version

0.80.0

Is any other VPN software installed?

Wireguard (the problem persists even when I disable the VPN)

Debug output

From netbird debug for 1m -AS -U: f79e391890ab27fb37c88b3b4be7011e22aa2e5ca6f38ffa9c4481884941f726/7943ed1b-b4cb-40d0-8eb5-412086ac0744

Screenshots

N/A

Additional context

Add any other context about the problem here.

Have you tried these troubleshooting steps?

  • Reviewed client troubleshooting (if applicable)
  • Checked for newer NetBird versions
  • Searched for similar issues on GitHub (including closed ones)
  • Restarted the NetBird client
  • Disabled other VPN software
  • Checked firewall settings

Similar GitHub issues

Additionally, here’s the nginx config I’m using:

/etc/nginx/sites-available/mastodon
map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

upstream backend {
    server 127.0.0.1:3000 fail_timeout=0;
}

upstream streaming {
    server 127.0.0.1:4000 fail_timeout=0;
}

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g;

server {
   listen 80;
   listen [::]:80;
   server_name <custom domain>;

   keepalive_timeout    70;
   sendfile             on;
   client_max_body_size 128m;
 
   root /srv/mastodon/public;
 
   gzip on;
   gzip_disable "msie6";
   gzip_vary on;
   gzip_proxied any;
   gzip_comp_level 6;
   gzip_buffers 16 8k;
   gzip_http_version 1.1;
   gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
 
   add_header Strict-Transport-Security "max-age=31536000";
 
   location / {
     if ( $http_user_agent ~* 'fedicrawl' ) {
       return 429;
     }
     try_files $uri @proxy;
   }
 
   location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) {
     add_header Cache-Control "public, max-age=31536000, immutable";
     add_header Strict-Transport-Security "max-age=31536000";
     try_files $uri @proxy;
   }
 
   location /sw.js {
     add_header Cache-Control "public, max-age=0";
     add_header Strict-Transport-Security "max-age=31536000";
     try_files $uri @proxy;
   }
 
   location @proxy {
     proxy_set_header Host $host;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-Proto https;
     proxy_set_header Proxy "";
     proxy_pass_header Server;
 
     proxy_pass http://backend;
     proxy_buffering on;
     proxy_redirect off;
     proxy_http_version 1.1;
     proxy_set_header Upgrade $http_upgrade;
     proxy_set_header Connection $connection_upgrade;
 
     proxy_cache CACHE;
     proxy_cache_valid 200 7d;
     proxy_cache_valid 410 24h;
     proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
     add_header X-Cached $upstream_cache_status;
     add_header Strict-Transport-Security "max-age=31536000";
 
     tcp_nodelay on;
   }
 
   location /api/v1/streaming {
     proxy_set_header Host $host;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-Proto https;
     proxy_set_header Proxy "";
 
     proxy_pass http://streaming;
     proxy_buffering off;
     proxy_redirect off;
     proxy_http_version 1.1;
     proxy_set_header Upgrade $http_upgrade;
     proxy_set_header Connection $connection_upgrade;
 
     tcp_nodelay on;
   }

   location /MathJax {
     alias /var/www/html/MathJax;
   }

   location /mathjax-newcm-font {
     alias /var/www/html/mathjax-newcm-font;
   }
 
   error_page 500 501 502 503 504 /500.html;
 }

This was my bad — I thought the streaming service was enabled, but it turns out it wasn’t, and I was looking at the wrong systemd logs. I managed to start the streaming service and it works fine, this had nothing to do with Netbird.