Plex Behind An Nginx Reverse Proxy: The Hard Way

 Uncategorized  Comments Off on Plex Behind An Nginx Reverse Proxy: The Hard Way
May 082024
 

Did IT deploy a new web filter at work? Is it preventing you from streaming music to drown out the droning of your co-workers taking meetings in your open plan office? Have you got a case of the Mondays?

That was the situation I found myself in recently. By default, Plex listens on port 32400, though it’ll happily use any port and it plays nice with gateways that support UPNP/NAT-PMP and pick a random public port to forward. That random port was the source of my problem. The new webfilter doesn’t mind the Plex domain, but it doesn’t like connections that aren’t on ports 80 or 443 – not even 22, and certainly not 32400.

Time for a reverse proxy. There’s lots of documentation about putting Plex behind a reverse proxy out there, but as is often the case with me, I had some additional requirements that complicated things a bit.

I already run a reverse proxy on my public IP that terminates TLS for a few services I host internally on my LAN behind an OAuth proxy. And by default, the connections from Plex clients want to connect directly to the media server via the plex.direct domain, which I don’t control and for which I can’t easily create TLS certificates (in truth, I probably could using Lets Encrypt and either the HTTP or ALPN challenge, but where’s the fun in that?).

Here’s the behaviour I need:
1. Stream connections for *.plex.direct to the Plex media server
2. Terminate TLS for primary domain name and proxy those connections internally
3. (Optional) Accept SSH connections on 443 and stream those to OpenSSH

First, create a new HTTPS proxy entry for plex, and update all of your proxies to use an alternate port. For fun, create a server entry that returns HTTP status code 418 – we’ll use that as a default fallthrough for connections we aren’t expecting.

http {
    server {
        listen 127.0.0.1:8443 ssl http2;
        server_name  wan.example.com;
        location / {
          proxy_pass https://home.lan.example.com;
        }
    }
    server {
        listen 127.0.0.1:8443 ssl http2;
        server_name  plex.example.com;
        location / {
          proxy_pass https://plex.lan.example.com:32400;
        }
    }
    server {
      listen 127.0.0.1:8080 default_server;
      return 418;
    }
}

Combine that with the Custom server access URLs setting and you’re probably good. But where’s the fun in that? We want maximum flexibility and connectivity from clients, so let’s mix it up with the stream module.

stream {
  log_format stream '$remote_addr - - [$time_local] $protocol '
                    '$status $bytes_sent $bytes_received '
                    '$upstream_addr "$ssl_preread_server_name" '                    
                    '"$ssl_preread_protocol" "$ssl_preread_alpn_protocols"';

  access_log /var/log/nginx/stream.log stream;

  upstream proxy {
    server      127.0.0.1:8443;
  }

  upstream teapot {
    server      127.0.0.1:8080;
  }

  upstream plex {
    server      172.16.10.10:32400;
  }

  upstream ssh {
    server      127.0.0.1:22;
  }

  map $ssl_preread_protocol $upstream {
    "" ssh;
    "TLSv1.3" $name;
    "TLSv1.2" $name;
    "TLSv1.1" $name;
    "TLSv1" $name;
    default $name;
  }

  map $ssl_preread_server_name $name {
    hostnames;
    *.plex.direct       plex;
    plex.example.com    proxy;
    wan.example.com     proxy;
    default             teapot;
  }

  server {
    listen      443;
    listen      [::]:443;
    proxy_pass  $upstream;
    ssl_preread on;
  }
}

Reading from the bottom we see that we’re listening on port 443, but not terminating TLS. We enable ssl_preread, and proxy_pass via $upstream. That uses the $ssl_preread_protocol map block to identify SSH traffic and send that to the local SSH server, otherwise traffic goes to $name.

$name uses the $ssl_preread_server_name map block, which uses the SNI name to determine which proxy to send the traffic to. Because we specify the hostnames variable, we can use wildcards in our domain matches. Connections for *.plex.direct stream directly to the Plex media server, while those for my domain name are streamed to the HTTPS reverse proxy I defined previously, which handles the TLS termination. Finally, any connection for a domain I don’t recognize gets a lovely 418 I’m a Teapot response code.

 Posted by at 2:32 AM