17

Right now there's an application that allows people to connect to a Desktop app through the web by exposing an AngularJS web server powered by Atmosphere. The Desktop app exposes the current person's IP address so anyone with the address can connect to.

I'm trying to mask this IP by proxying it through my server (example.com). My server currently hosts a series of applications (Ruby on Rails + Elastic Search, Logstash, Kibana - ELK) and are proxied by an Nginx client.

I've worked out to successfully mask the IP addresses through Node HTTP Proxy (locally), and now I'm trying to make it work while using Nginx. The AngularJS app uses Websockets so I need to proxy both the HTTP and WS request.

See this diagram: enter image description here

I'm very close to figuring everything out. I've tested locally without Nginx and the IP addresses are getting masked correctly. I'm having a challenge to make Nginx redirect the HTTP and Websockets through the same location (See code).

From all the tutorials and Server Fault posts I've seen that Websockets usually point to a different location, and Nginx gracefully upgrades the connection.

I'm having a challenge right now, that I'm trying to proxy through the same location HTTP/2 and Websockets protocols. I've resorted to evil hacks such as using IF inside the location blocks (but they haven't worked).

Ideally I'd like to have Websockets point to a different location than the HTTP, and that would solve the problem. My current problem is that I don't have the source code to the AngularJS App in order for me to do so.

The Atmosphere server seems to detect the Websockets connection through query parameters (This is the URL that it connects to):

ws://the-user-ip/?X-Atmosphere-tracking-id=0&X-Atmosphere-Framework=2.3.2-javascript&X-Atmosphere-Transport=websocket&Content-Type=application/json&X-atmo-protocol=true. 

Here's part of my current configuration from Nginx:

upstream ipmask_docker_app { server ipmask:5050; } server { server_name "~^\d+\.example\.co$"; # listen 80; listen 443 ssl http2; listen [::]:443 ssl http2; # HTTPS config omitted due to conciseness. location / { # https://www.digitalocean.com/community/questions/error-too-many-redirect-on-nginx # proxy_ignore_headers X-Accel-Expires Expires Cache-Control; # proxy_ignore_headers Set-Cookie; # proxy_hide_header Set-Cookie; # proxy_hide_header X-powered-by; # 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 Host $http_host; proxy_pass http://ipmask_docker_app; proxy_http_version 1.1; # Enables Websockets # https://www.nginx.com/blog/websocket-nginx/ # https://stackoverflow.com/a/46675414/1057052 # Have the http_version 1.1 disabled. I want to know if it works # THIS IS EVIL: set $ws_header_upgrade ''; set $ws_value_upgrade ''; set $ws_header_connection ''; proxy_set_header 'Debug Header' $query_string; if ($args ~* "X-Atmosphere-tracking-id") { set $ws_header_upgrade Upgrade; set $ws_value_upgrade $http_upgrade; set $ws_header_connection "Upgrade"; } proxy_set_header $ws_header_upgrade $ws_value_upgrade; proxy_set_header Connection $ws_header_connection; # limit_req zone=one; access_log /var/www/cprint/log/nginx.access.log; error_log /var/www/cprint/log/nginx.error.log; } } 

In the code above I can't seem to have proxy_set_header Host $http_host; and proxy_set_header Upgrade $http_upgrade in the same location block. That's why I tried matching unsuccessfully the query_string of X-Atmosphere-tracking-idand setting the headers to upgrade it in case that it matches it.

Otherwise, if I upgrade the connection I'm not able to see the Web Page loaded as it doesn't seem to proxy the HTTP protocol but the WS.

Is the only way to upgrade Websockets is to have it point to a different location? Or is there a way to upgrade both (HTTP and WS) by pointing them at the same place?

Thank you!

4
  • 1
    You really should have a unique path for your websocket endpoint. Commented May 4, 2018 at 21:25
  • @MichaelHampton: I'll have to speak with the dev in charge of that portion. I think that's the best way as well. Commented May 4, 2018 at 21:39
  • I ended up changing the URL to /ws Commented Jul 24, 2018 at 17:43
  • Just to reiterate, I ended up solving the problem by changing the WebSocket's URL to point to a different location. After that, Nginx managed to distinguish between the HTTP protocal and the WS and successfully masked the address with HTTPS. Commented Jul 2, 2019 at 19:06

2 Answers 2

28

The easiest way I found was to jump to different locations based on the "Upgrade" header:

server { # ... location / { try_files /nonexistent @$http_upgrade; } location @websocket { # websocket related stuff } location @ { # web related stuff } } 
9
  • 1
    You can use $http_upgrade directly on try_files, @websocket for your ws location and just @ for your http location, avoiding the need for a map. Commented Mar 29, 2019 at 15:02
  • Updated based on your suggestion. Thanks! Commented Apr 1, 2019 at 19:58
  • That is a dirty little trick, nice! Commented Jun 19, 2020 at 13:36
  • 2
    @mkg20001, @MacroMan that's a great answer. Adding a few words to explain how it works would be even better. A little googling on try_files and "named locations" (@ prefix) helped me figure out what was going on. Nice answer. Commented Jan 14, 2022 at 21:25
  • 1
    In the above, try_files /nonexistent @$http_upgrade will fail on /nonexistent and move on to resolve @$http_upgrade. The var $http_upgrade will contain the exact string websocket if the connection is a websocket. So either location @ or location @websocket will be chosen depending on whether the connection is a websocket or not. Commented Feb 28, 2022 at 20:16
2

The following config proxy WebSocket traffic to http://localhost:7890/

Ref: https://steelywing.github.io/note/Note/Nginx/WebSocket/

map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80; server_name example.com; location /websocket/ { # internal location only can access by rewrite, if client open /websocket/ will return 404 internal; # must end with "/", see https://steelywing.github.io/note/Note/Nginx/Proxy/ proxy_pass http://localhost:7890/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } location / { set $websocket 1; if ($http_connection !~* "upgrade") { set $websocket 0; } if ($http_upgrade !~* "websocket") { set $websocket 0; } if ($websocket) { rewrite ^ /websocket$uri last; } # ... } } 
3
  • This is actually working better for me than the accepted answer, because you can have proxy_pass directives for both the websocket and the normal requesets. Commented Apr 6, 2023 at 11:21
  • Is this use of the "if" directive safe? Commented Aug 24, 2023 at 4:56
  • 1
    @Xunnamius If you just set variable and rewrite in if, it is safe, for detail, see steelywing.github.io/note/Note/Nginx/if/#flow Commented Sep 1, 2023 at 3:02

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.