-
-
Save doonga/dc91f2c52f17c9931e269966dfcb956d to your computer and use it in GitHub Desktop.
Credit where credit's due, I mainly used these 3 resources to get this all working. There were several others but I don't exactly remember which is which. So at least here are the 3 main places that I used. | |
The vast majority of this came from this link, the others helped me make it work for Plex. | |
http://emby.media/community/index.php?/topic/30975-reverse-proxy-with-ssl-hostname-routing-and-embyopenvpn-port-sharing/ | |
https://gist.github.com/spikegrobstein/4384954 | |
https://forums.plex.tv/discussion/207725/ubuntu-14-04-4-lts-plex-incorrectly-handles-lower-case-headers#latest | |
The data flow here is: | |
User -> Cloudflare -> Origin IP -> Haproxy -> NGINX -> Plex | |
All caching and acceleration stuff is turned off on Cloudflare. I'm using Full(Strict) with Authenticated Origin pulls and HSTS. I'm mainly using it to obscure my real IP as well as get better peering for the few family members that I share with. Plus I wanted to see if I could make it work. | |
I have a fully automated Letsencrypt setup for NGINX, which isn't completely necessary, but when connecting locally I wanted trusted certs without having to roll my own CA and load the signer certificate everywhere. Also if I decide to stop using Cloudflare, then I'll have "real" certs on my endpoints so there won't be any trust issues. | |
I'm also using split DNS so hostnames within my home network resolve directly to the server hosting Haproxy. | |
Note: This works with the web client, Android TV, FireTV, Android. Chromecast via Android Plex doesn't work, if you use the webclient to cast to Chromecast it works fine. |
global | |
log /dev/log local0 | |
log /dev/log local1 notice | |
chroot /var/lib/haproxy | |
user haproxy | |
group haproxy | |
daemon | |
defaults | |
log global | |
option dontlognull | |
option http-server-close | |
timeout http-request 5s | |
timeout connect 5000 | |
timeout client 30000 | |
timeout server 30000 | |
frontend tls_router | |
bind *:443 | |
mode tcp | |
option tcpka | |
# Define known networks for later use | |
acl local src 127.0.0.0/8 192.168.1.0/24 | |
acl cloudflare src 199.27.128.0/21 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/12 172.64.0.0/13 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 | |
#Rate-limiting measures, in case of DoS | |
# Table for connection tracking | |
stick-table type ip size 100k expire 30s store conn_cur | |
# Allow known CloudFlare IPs to bypass the rate-limiting | |
tcp-request connection accept if cloudflare | |
# Reject connection if client has more than 10 open | |
tcp-request connection reject if { src_conn_cur ge 10 } | |
tcp-request connection track-sc1 src | |
# Max inspection delay for SNI routing | |
tcp-request inspect-delay 2s | |
# Accept only SSL/TLS traffic | |
tcp-request content accept if { req_ssl_hello_type 1 } | |
# If SNI hostname is known server AND is from known IP, pass to backend | |
# Plex | |
acl plex_sni req_ssl_sni -i plex.example.com | |
# Root hostname | |
acl root_sni req_ssl_sni -i example.com | |
use_backend plex_tls if plex_sni cloudflare or plex_sni local | |
use_backend example_tls if root_sni cloudflare or root_sni local | |
default_backend example_tls | |
backend plex_tls | |
mode tcp | |
option tcpka | |
server server_plex_tls localhost:7443 send-proxy | |
backend example_tls | |
mode tcp | |
option tcpka | |
server server_example_tls localhost:443 send-proxy |
This is a nice proxylog line | |
log_format proxyLog '[$time_local] $real_ip - $remote_user - $server_name to: $upstream_addr: $status / upstream $upstream_status $request upstream_re | |
sponse_time $upstream_response_time msec $msec request_time $request_time body: $request_body'; |
# plex reverse proxy | |
# | |
# Intended to sit downstream from HAProxy, uses proxy protocol. | |
# Allows traffic from the local network, or from WAN w/ CloudFlare client cert | |
# | |
server { | |
# Using proxy protocol to get client info passed from HAProxy | |
listen 127.0.0.1:7443 ssl http2 proxy_protocol; | |
listen [::1]:7443 ssl http2 proxy_protocol; | |
keepalive_timeout 180; | |
proxy_bind 127.1.1.1; | |
# Log Rewrites | |
rewrite_log on; | |
access_log /var/log/nginx/plex_example_com.log proxyLog; | |
#error_log /var/log/nginx/plex_error.log debug; | |
# Who the hell am I?! Where's my stuff?! | |
server_name plex.example.com; | |
root /var/www/plex.example.com/public_html; | |
# Import some solid TLS settings | |
include snippets/solidtls.conf; | |
# Certs | |
ssl_certificate /srv/letsencrypt/certs/plex.example.com/fullchain.pem; | |
ssl_certificate_key /srv/letsencrypt/certs/plex.example.com/privkey.pem; | |
ssl_trusted_certificate /srv/letsencrypt/certs/plex.example.com/fullchain.pem; | |
ssl_client_certificate /etc/nginx/ssl/cf-origin-pull-ca.pem; | |
# Client cert optional. We'll do our own access control rules in / | |
ssl_verify_client optional; | |
# Let's get the real client info from upstream proxy | |
include snippets/realip-cf-haproxy.conf; | |
# Set headers with real client info for downstream app | |
proxy_set_header Host $host; | |
proxy_set_header X-Real-IP $real_ip; | |
proxy_set_header X-Forwarded-For $real_ip; | |
proxy_set_header X-Forwarded-Proto $real_ip; | |
proxy_set_header X-Forwarded-Protocol $scheme; | |
# Turn off buffering for streaming purposes | |
proxy_buffering off; | |
# No request rewriting | |
proxy_redirect off; | |
# Send websocket data to the backend, courtesy @[member="Karbowiak"] on Emby forum | |
proxy_http_version 1.1; | |
proxy_set_header Upgrade $http_upgrade; | |
proxy_set_header Connection "upgrade"; | |
# Plex headers | |
# As of 0.9.17.0 this shouldn't be needed anymore since Plex now ignores case in headers | |
# proxy_set_header X-Plex-Client-Identifier $http_x_plex_client_identifier; | |
# proxy_set_header X-Plex-Device $http_x_plex_device; | |
# proxy_set_header X-Plex-Device-Name $http_x_plex_device_name; | |
# proxy_set_header X-Plex-Platform $http_x_plex_platform; | |
# proxy_set_header X-Plex-Platform-Version $http_x_plex_platform_version; | |
# proxy_set_header X-Plex-Product $http_x_plex_product; | |
# proxy_set_header X-Plex-Token $http_x_plex_token; | |
# proxy_set_header X-Plex-Version $http_x_plex_version; | |
# proxy_set_header X-Plex-Nocache $http_x_plex_nocache; | |
# proxy_set_header X-Plex-Provides $http_x_plex_provides; | |
# proxy_set_header X-Plex-Device-Vendor $http_x_plex_device_vendor; | |
# proxy_set_header X-Plex-Model $http_x_plex_model; | |
# proxy_set_header X-Plex-Container-Start $http_x_plex_container_start; | |
# proxy_set_header X-Plex-Container-Size $http_x_plex_container_size; | |
# Test if IP from HAProxy is local - tweak to match your network | |
set $is_local_client false; | |
if ( $proxy_protocol_addr ~ 192.168.1.* ) { | |
set $is_local_client true; | |
} | |
# Force the use of our custom robots.txt | |
location /robots.txt { | |
try_files $uri /var/www/plex.example.com/public_html/robots.txt; | |
} | |
# Catch-all. Performs authentication based on client IP or certificate. | |
location / { | |
# Redirect if not an options request. | |
if ($request_method != OPTIONS ) { | |
set $test A; | |
} | |
if ($http_x_plex_device_name = '') { | |
set $test "${test}B"; | |
} | |
if ($arg_X-Plex-Device-Name = '') { | |
set $test "${test}C"; | |
} | |
if ($test = ABC) { | |
rewrite ^/$ https://$http_host/web/index.html; | |
} | |
# We'll return 418 later if client is authorized | |
error_page 418 = @authorized; | |
#Access rules | |
# Allow local clients without client cert | |
if ( $is_local_client = true ) { return 418; } | |
# Does client have valid client certificate? | |
if ( $ssl_client_verify = "SUCCESS" ) { return 418; } | |
# Deny all others, return 403 unauthorized | |
return 403; | |
} | |
# Handles 418 responses for authorized users. Does the actual proxying. | |
# Using the real IP of the Plex server instead of localhost | |
location @authorized { | |
proxy_pass https://192.168.1.20:32400; | |
} | |
} |
#Local traffic, including HAProxy on localhost | |
set_real_ip_from 127.0.0.0/8; | |
#CloudFlare | |
#These need to be updated from Cloudflare as they may have changed | |
set_real_ip_from 199.27.128.0/21; | |
set_real_ip_from 173.245.48.0/20; | |
set_real_ip_from 103.21.244.0/22; | |
set_real_ip_from 103.22.200.0/22; | |
set_real_ip_from 103.31.4.0/22; | |
set_real_ip_from 141.101.64.0/18; | |
set_real_ip_from 108.162.192.0/18; | |
set_real_ip_from 190.93.240.0/20; | |
set_real_ip_from 188.114.96.0/20; | |
set_real_ip_from 197.234.240.0/22; | |
set_real_ip_from 198.41.128.0/17; | |
set_real_ip_from 162.158.0.0/15; | |
set_real_ip_from 104.16.0.0/12; | |
set_real_ip_from 172.64.0.0/13; | |
set_real_ip_from 2400:cb00::/32; | |
set_real_ip_from 2606:4700::/32; | |
set_real_ip_from 2803:f800::/32; | |
set_real_ip_from 2405:b500::/32; | |
set_real_ip_from 2405:8100::/32; | |
#Default state, assume local traffic/proxy protocol header | |
set $forwarded_ip_header proxy_protocol; | |
set $real_ip $proxy_protocol_addr; | |
# Is Cloudflare header set? Use that header/addr instead of proxy_protocol | |
if ($http_cf_connecting_ip) { | |
set $forwarded_ip_header CF-Connecting-IP; | |
set $real_ip $http_cf_connecting_ip; | |
} | |
# Tell Nginx which header to use | |
real_ip_header $forwarded_ip_header; |
# Trustworthy SSL/TLS settings in a box | |
# | |
ssl on; | |
gzip off; # gzip allows for some attacks | |
# General params | |
ssl_session_timeout 1d; | |
ssl_session_cache shared:SSL:10m; | |
ssl_session_tickets off; | |
# Always use custom-generated DH params >2048 bits | |
ssl_dhparam /etc/nginx/ssl/dhparam.pem; | |
#Intermediate cipher config | |
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; | |
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; | |
#Modern cipher config | |
#ssl_protocols TLSv1.1 TLSv1.2; | |
#ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK'; | |
#Perfect ssllabs score cipher list. Not super usable, works for CF-only setups | |
#ssl_protocols TLSv1.2; | |
#ssl_ciphers AES256+EECDH:AES256+EDH:!aNULL; | |
# No downgrade attacks | |
ssl_prefer_server_ciphers on; | |
# HSTS | |
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; | |
add_header X-Frame-Options DENY; | |
add_header X-Content-Type-Options nosniff; | |
# Stapling | |
ssl_stapling on; | |
ssl_stapling_verify on; | |
# DNS resolvers to use for cert verification | |
resolver 8.8.8.8 8.8.4.4 valid=300s; | |
resolver_timeout 5s; |
Did you portforward 32400 on your local network, I believe that is why mine wasn't working. If you didn't then that would mean that my plex would auto resolve to the IP and port 32400 whereas if yours couldn't reach the internet then it would force itself to use the custom url.
I had my custom server url set as well. What do you mean by making sure the public port matches haproxy? in the url? in the server settings? my custom url although was set to https://plex.mydomain.com:443 with the port.
No I only have 443 port forwarded. On the Remote Access tab in the Plex settings, make sure you set the remote port manually as 443. The Custom Server Access URL doesn't need the port on it, that's the purpose of the port number on the Remote Access tab.
Hi! This works for me, thank you so much!
Is there a way to run other HTTPS services on the same machine though? (Such as PlexRequests or rutorrent)
I assume I'd have to change something in the haproxy config, I'm not sure what however.
I managed to get your code working; however, like @camjac251 my stream isn't being passed through cloudflare, just the library itself.
tcptrack reveals that during playback, the source is serverip:32400, so effectively my server's ip is still being leaked. Blocking port 32400 has the effect of stopping media playback; however, I can still browse the media library with thumbnails loading for all objects.
I have set the custom url and public port to 443, yet it doesn't seem to affect it overall. I'm going to keep tweaking some stuff on my end, but not sure there's anyway that it can be forced.
Chromecast via Android Plex doesn't work, if you use the webclient to cast to Chromecast it works fine.
This is true, with and without this Nginx solution. I tried using Nginx just to try something else because even without it I al ways get "Something went wrong" in Plex whenever I cast from my Android app. However, if i login from the phone browser and cast from there it works just fine.
Thank you!
Yep I have it working just fine. A note though, better peering probably won't get you too much in better speeds (in the US at least.) It really is there to help reduce jitter and to hide your real IP.
You do need to go into Plex and make your public port match what you are using in haproxy which should be 443. You also need to make your Custom Server Access URLs: https://plex.yourdomain.com (or whatever you use, the https:// part is what's important.)