Didstopia / rust-server

Provides a dedicated linux server for Rust (the game) running inside a Docker container.
MIT License
243 stars 105 forks source link

RCON over secure websocket (wss) #101

Closed Iyashi closed 1 year ago

Iyashi commented 2 years ago

Is your feature request related to a problem? Please describe.

Currently the rcon web frontend has hardcoded ws:// protocol and thus does not support secure websockets (wss:// protocol)

I got it working and wanted to share my solution here so that it may help in finding a clean and permanent solution or at least help those who might encounter this problem as well.

The problematic line of code is in container /usr/share/nginx/html/js/rconService.js

  Service.Connect = function(addr, pass) {
    this.Socket = new WebSocket("ws://" + addr + "/" + pass); // <-- here
    this.Address = addr;

Describe the solution you'd like

I used a solution with a custom subdomain specificly for the websocket. (ws.rcon.rust.example.com) It should also be possible with rcon.rust.example.com:28016 if you fix a few things here. (Shouldn't be that much of a problem, but if it is, just ask and I'll provide a working example here)

Create sub domains

I used the following domains

Fix the rcon web client

  1. start Rust container and download /usr/share/nginx/html/js/rconService.js:
    docker-compose up -d
    # you may have to wait for startup before the file becomes available (since some stuff is downloaded at container startup)
    docker cp <container>:/usr/share/nginx/html/js/rconService.js rconService.js
  2. edit local rconService.js:
     Service.Connect = function(addr, pass) {
       addr = addr.replace(/^www./i, '').split(':')[0] // trimPrefix "www" and remove port
       var wsurl = "wss://ws." + addr + "/";
       console.log("Websocket url:", wsurl);
       this.Socket = new WebSocket(wsurl + pass);
       this.Address = addr;
  3. add rconService.js as volume mount in Rust's docker-compose.yml:
       volumes:
         - ./rconService.js:/usr/share/nginx/html/js/rconService.js

Full docker-compose setup

  1. Traefik docker network: docker network create --attachable traefik
  2. Full Traefik docker-compose.yml (with letsencrypt): NOTE: Change your@e.mail!

    version: "3.3"
    
    networks:
     traefik:
       external: true
    
    services:
    
     traefik:
       image: traefik:v2.5
       command:
         - "--log.level=INFO"
         - "--providers.docker=true"
         - "--providers.docker.exposedbydefault=false"
         - "--entrypoints.web.address=:80"
         - "--entrypoints.websecure.address=:443"
         - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
         - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
         - "--certificatesresolvers.letsencrypt.acme.email=your@e.mail"
         - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
       networks:
         - traefik
       ports:
         - 80:80
         - 443:443
       volumes:
         - ./letsencrypt:/letsencrypt
         - /var/run/docker.sock:/var/run/docker.sock:ro
  3. Full Rust docker-compose.yml: NOTE: Change example.com to your domain!

    version: "3.3"
    
    networks:
     default:
     traefik:
       external: true
    
    services:
     rust:
       image: didstopia/rust-server
       volumes:
         - ./rust_data:/steamcmd/rust
         - ./rconService.js:/usr/share/nginx/html/js/rconService.js # fixed secure websocket (wss://)
       networks:
         - default
         - traefik
       ports:
         - 0.0.0.0:28015:28015
         - 0.0.0.0:28015:28015/udp
         - 0.0.0.0:28016:28016
         #- 0.0.0.0:8080:8080 # portforwarding using traefik (see last labels)
       environment:
         # ... omitted ...
       labels:
         - traefik.enable=true
         - traefik.docker.network=traefik
         # rcon web (:8080 => rcon.rust.example.com or www.rcon.rust.example.com)
         - traefik.http.routers.game-rust-rcon.rule=Host(`rcon.rust.example.com`) || Host(`www.rcon.rust.example.com`)
         - traefik.http.routers.game-rust-rcon.entrypoints=websecure
         - traefik.http.routers.game-rust-rcon.tls.certresolver=letsencrypt
         - traefik.http.routers.game-rust-rcon.service=game-rust-rcon
         - traefik.http.services.game-rust-rcon.loadbalancer.server.port=8080
         # rcon websocket (:28016 => ws.rcon.rust.example.com)
         - traefik.http.routers.game-rust-rcon-ws.rule=Host(`ws.rcon.rust.example.com`)
         - traefik.http.routers.game-rust-rcon-ws.entrypoints=websecure
         - traefik.http.routers.game-rust-rcon-ws.tls.certresolver=letsencrypt
         - traefik.http.routers.game-rust-rcon-ws.service=game-rust-rcon-ws
         - traefik.http.services.game-rust-rcon-ws.loadbalancer.server.port=28016

Hope it helps

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Dids commented 2 years ago

Thanks for this. I originally played around with this as well, but never got it to work.

For now, I pushed an update that adds RUST_RCON_SECURE_WEBSOCKET as a new environment variable, defaulting to 0, but if you set it to 1 it will replace the insecure protocol (ws://) protocol with the secure one (wss://).

Hopefully this helps!

josh-sachs commented 2 years ago

Took me a while to get this working...

There was no situation where the secure websocket setting worked, in fact it broke accessing rcon locally. Just set it to 0 in docker-compose.yml for rust.

RUST_RCON_SECURE_WEBSOCKET=0

Next, I applied similar, but different Iyashi's edits to rconService.js

 // remove port - we want to go through the default ssl entrypoint in traefik (443).
addr = addr.split(':')[0]; 

// infer websocket protocol from current http/s protocol (if you're on https, you are forbidden from using ws:// anyway).
if (window.location.protocol === "https:")
  this.Socket = new WebSocket("wss://" + addr + "/" + pass);  
else
  this.Socket = new WebSocket("ws://" + addr + "/" + pass);  

// unchanged
this.Address = addr;

finally, in traefik:

 http:
  services:
    rust-rcon-web:
      loadBalancer:
        servers:
          - url: http://192.168.2.98:28080

    rust-rcon-ws:
      loadBalancer:
        servers:
          - url: http://192.168.2.98:28016  # notice we're not pointing at https... traefik is ssl offloading.

  routers:
    rust-rcon-web-rtr:
      service: rust-rcon-web
      rule: Host(`rust.domain.com`)
      entryPoints:
        - https

    rust-rcon-ws-rtr:
      service: rust-rcon-ws
      rule: Host(`rust.domain.com`) && Headers(`X-Forwarded-Proto`, `wss`)  # adding a rule looking for the forwarded proto means we don't need a new subdomain for websocket.
      entryPoints:
        - https

I think the only change that should happen to this image is in rconService.js

Ideally, add my conditional logic for selecting web socket protocol.

In the docker config, there should be two settings for rcon port...

RUST_RCON_SERVER_PORT=28016 RUST_RCON_CLIENT_PORT=443 this could default to RUST_RCON_SERVER_PORT if not provided. Then, the client port needs to be what is used when calling `rconService.js Service.Connect()

Honestly, this setting should be deleted (it doesn't work, and adds confusion). RUST_RCON_SECURE_WEBSOCKET

I may try to get a PR together for you.

Hyp3rSoniX commented 2 years ago

I miserably failed to get the rcon Web to work with my nginx setup...

The main problem seems to be, that the websocket and the web port are different... If the websocket connection would be built on the web port, a simple reverse proxy could get things running without any problems.

My first approach to get the Web Interface onto the Web worked, however connecting to the Websocket failed because of "https and ws can't be mixed up".

Simply activating RUST_RCON_SECURE_WEBSOCKET also didn't help, since then the intern websocket call expects certificates and stuff. Simply changing ws to wss doesn't work with the same reason why changing http to https without providing the necessary certs wont work.

If anyone managed to get the rcon stuff to work with nginx, please help. Until then, I'm pretty much forced to use rcon in my local network only.

stale[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.