Отлучаем сервисы от 443-го порта - (не)Уникальный опыт

Отлучаем сервисы от 443-го порта


Все любят 443-й порт. А некоторые еще и 80-й. Короче таких любителей много, а порт один и повесить все сервисы на один порт невозможно. Ну вообще возможно все. Если захотеть. И использовать правильные инструменты.

Есть у меня Raspberry Pi на котором крутились разные сервисы и до поры не мешали друг-другу. 80-й и 443-й порты занимал zwift-offline. Я понимаю по какой причине они захардкодили эти порты, но не понимаю почему они не оставили возможности их изменения для работы через прокси. Но мы это починим и довольно скоро. Так вот, жил у меня весь этот зоопарк пока не появилась необходимость поднять еще один подобный сервис, который тоже хочет 443-й порт, да еще и со своим сертификатом. Ну в общем требования такие же как у zwift-offline. Осталось придумать как все это засунуть в бедную Raspberry Pi и заставить работать.

/img/haproxyfying-them-all/kdpv.png

💬
Напомню, что в *nix все порты до 1000 являются привелегированными и для их открытия нужны root права. Т.е. для запуска zwift-offline нужен root. Это не большая проблема в моем случае, но все же хотелось бы давать такие права как можно меньшему количеству сервисов.

Чиним Zwift

Нужно добавить отсутствующую возможность запуска сервиса на любых портах отличных от 80 и 443. Для этого нужно применить такой патч к существующему коду:

Subject: [PATCH] Add port customization feature
---
Index: standalone.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/standalone.py b/standalone.py
--- a/standalone.py	(revision 184d07534a7e9740873b8d35c9455419ba0ef456)
+++ b/standalone.py	(date 1732480060765)
@@ -781,7 +781,7 @@
     bot.start()
 
 socketserver.ThreadingTCPServer.allow_reuse_address = True
-httpd = socketserver.ThreadingTCPServer(('', 80), CDNHandler)
+httpd = socketserver.ThreadingTCPServer(('', os.environ.get('ZWIFT_HTTP_PORT', 80)), CDNHandler)
 zoffline_thread = threading.Thread(target=httpd.serve_forever)
 zoffline_thread.daemon = True
 zoffline_thread.start()
Index: zwift_offline.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/zwift_offline.py b/zwift_offline.py
--- a/zwift_offline.py	(revision 184d07534a7e9740873b8d35c9455419ba0ef456)
+++ b/zwift_offline.py	(date 1732480069047)
@@ -4067,7 +4067,7 @@
     send_message_thread = threading.Thread(target=send_server_back_online_message)
     send_message_thread.start()
     logger.info("Server version %s is running." % ZWIFT_VER_CUR)
-    server = WSGIServer(('0.0.0.0', 443), app, certfile='%s/cert-zwift-com.pem' % SSL_DIR, keyfile='%s/key-zwift-com.pem' % SSL_DIR, log=logger)
+    server = WSGIServer(('0.0.0.0', os.environ.get('ZWIFT_HTTPS_PORT', 443)), app, certfile='%s/cert-zwift-com.pem' % SSL_DIR, keyfile='%s/key-zwift-com.pem' % SSL_DIR, log=logger)
     server.serve_forever()
 
 #    app.run(ssl_context=('%s/cert-zwift-com.pem' % SSL_DIR, '%s/key-zwift-com.pem' % SSL_DIR), port=443, threaded=True, host='0.0.0.0') # debug=True, use_reload=False)

Проверено на версии кода 184d075

Настройка проксирования

В качестве прокси я взял HAProxy, поэтому для начала установим его на RPi:

sudo apt install haproxy

Теперь нужно поднять проксю, которая будет разводить трафик по разным сервисам.

Главным требованием к проксику у меня было наличие SSL passthrough чтобы не жонглировать сертификатами. Да и в целом терминирование TLS мне не требовалось, потому что сервисы и так ожидают TLS. В результате некоторого количества проб и ошибок получился вот такой конфиг:

global
  ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11
  ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384

defaults
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

frontend http
  mode http
  bind *:80
  use_backend zoffline_http

frontend https
  mode tcp
  bind *:443 
  tcp-request inspect-delay 5s
  tcp-request content accept if { req_ssl_hello_type 1 }

  acl sni_ur_or_rly101_zwift_com req_ssl_sni -i us-or-rly101.zwift.com
  acl sni_secure_zwift_com req_ssl_sni -i secure.zwift.com
  acl sni_launcher_zwift_com req_ssl_sni -i launcher.zwift.com

  acl sni_another_service req.ssl_sni -i another.site.com

  use_backend zoffline_https if sni_ur_or_rly101_zwift_com
  use_backend zoffline_https if sni_secure_zwift_com
  use_backend zoffline_https if sni_launcher_zwift_com

  use_backend another_backend if sni_another_service

backend zoffline_http
  mode http
  server internal_zoffline_http 127.0.0.1:65100 check

backend zoffline_https
  mode tcp
  server internal_zoffline_https 127.0.0.1:65101 check

backend another_backend
  mode tcp
  server internal_another_backend 127.0.0.1:31337 check

Конечно же использование такого конфига требует правок в /etc/hosts на клиенте чтобы завернуть трафик на локальную машину при запросе доменов того же zwift или чего угодно другого.

В итоге zwift-offline был поднят на портах 65100 вместо 80 и 65101 вместо 443, а другой секретный сервис на порту 31337 вместо 443. И все это работает благодаря механизмам проксирования с поддержкой SSL passthrough. Если хочется кастомизаций, то добро пожаловать в официальную документацию по настройке.


Смотрите также