Как стать автором
Обновить

Маршрутизация силами Haproxy, DoH, GeoIP, защита сервисов через mTLS и выгрузка метрик в Prometheus, настройка ACME.SH

Уровень сложностиСложный
Время на прочтение38 мин
Количество просмотров2.6K
Всего голосов 9: ↑9 и ↓0+14
Комментарии6

Комментарии 6

ЗакрепленныеЗакреплённые комментарии

Дополнение к статье

VLESS TCP TLS

РКН начали ломать TLS, что бы обойти можно открыть диапазон портов, пример для секции f_tcp:

frontend f_tcp
        bind [::]:443 v4v6 # SSL Passthrough
        bind [::]:4000-9000 v4v6   # SSL Passthrough extended_1 ports // ufw allow 4000:9000/tcp
        bind [::]:15000-20000 v4v6 # SSL Passthrough extended_2 ports // ufw allow 15000:20000/tcp
        bind [::]:63000-65534 v4v6 # SSL Passthrough extended_4 ports // ufw allow 63000:65534/tcp

Пока Сибирь и ДВ жаловались на проблемы с REALITY, VLESS TLS нужно тестировать.

Создадим скрипт который будет мониторить обновление сертификата через сокет haproxy:

import subprocess
import re
import time
import os

# Путь к файлу для хранения даты сертификата
CERT_DATA_FILE = '/tmp/certdata.txt'
# Путь к сокету HAProxy
SOCKET_PATH = '/var/run/haproxy/admin.sock'
# Команда для получения информации о сертификате
COMMAND = f'echo "show ssl cert /etc/haproxy/certs/$вашдомен.pem" | nc -U {SOCKET_PATH}'
# Период проверки в секундах
CHECK_INTERVAL = 25

def get_cert_not_before():
    """Получает дату 'notBefore' из сертификата."""
    try:
        result = subprocess.check_output(COMMAND, shell=True, text=True)
        match = re.search(r'notBefore:\s*(.*)', result)
        if match:
            return match.group(1).strip()
    except subprocess.CalledProcessError as e:
        print(f"Error executing command: {e}")
    return None

def read_previous_cert_date():
    """Читает предыдущую дату сертификата из файла."""
    if os.path.exists(CERT_DATA_FILE):
        with open(CERT_DATA_FILE, 'r') as f:
            return f.read().strip()
    return None

def write_cert_date(date):
    """Записывает дату сертификата в файл."""
    with open(CERT_DATA_FILE, 'w') as f:
        f.write(date)

def restart_service():
    """Перезапускает сервис x-ui."""
    try:
        subprocess.run(['systemctl', 'restart', 'x-ui'], check=True)
        print("Сервис x-ui перезапущен.")
    except subprocess.CalledProcessError as e:
        print(f"Ошибка при перезапуске сервиса: {e}")

def main():
    """Основная функция для мониторинга сертификата."""
    while True:
        current_cert_date = get_cert_not_before()
        if current_cert_date:
            previous_cert_date = read_previous_cert_date()

            if current_cert_date != previous_cert_date:
                print(f"Дата сертификата изменилась: {previous_cert_date} -> {current_cert_date}")
                write_cert_date(current_cert_date)
                restart_service()
            else:
                print("Дата сертификата не изменилась.")

        time.sleep(CHECK_INTERVAL)

if __name__ == "__main__":
    main()

Теперь сделаем его исполняем chmod 700 certmon.py

Разместим в systemd
nano /etc/systemd/system/certmon.service

[Unit]
Description=Monitor HAProxy SSL Certificate

[Service]
ExecStart=/usr/bin/python3 /path/to/your/certmon.py
Restart=always

[Install]
WantedBy=multi-user.target

Для VLESS TLS необходимо настроить хранилище сертификатов таким же путем как и с haproxy, в директорию /etc/x-ui/ssl
После получим сертификат:

#!/bin/sh
DEPLOY_HAPROXY_HOT_UPDATE=yes \
DEPLOY_HAPROXY_STATS_SOCKET=/var/run/haproxy/admin.sock \
DEPLOY_HAPROXY_PEM_PATH=/etc/haproxy/certs \
acme.sh --issue -d $ВАШДОМЕН --stateless && \
acme.sh --install-cert -d $ВАШДОМЕН --cert-file /etc/x-ui/ssl/cert.pem \
--key-file /etc/x-ui/ssl/cert.key --fullchain-file /etc/x-ui/ssl/fullchain.ru.pem && \
acme.sh --deploy -d $ВАШДОМЕН --deploy-hook haproxy

Подождем и зайдем в панель, и настроим vless tls
Обязательно указываем в inbound, мониторинг IP /var/lib/haproxy/xui/vlesstls.sock,0660
Proxy Protocol = true
Sockopt = true
TCP Fast Open = true
Multipath TCP = true
External Proxy = true
Тот же / cloud.вашдомен / 443
Безопасность TLS 
SNI cloud.вашдомен
uTLS на ваш выбор
ALPN h2 http/1.1
И укажем путь к сертификату
Публичный ключ: /etc/x-ui/ssl/cert.pem
Закрытый ключ: /etc/x-ui/ssl/cert.key.
Теперь обязательно создадим fallback для маскировки,
В настройках inbound VLESS TLS добавляем fallback, sni alpn path оставляем пустыми, в Dest указываем /var/lib/haproxy/fakevless1.sock и xVer устанавливаем в 1.

Теперь настроим haproxy, настроим фронтенд tcp для отправки трафика на x-ui:

use_backend tcp_to_vlesstls if { req.ssl_sni -i cloud.вашдомен }

Теперь настроим backend:

backend tcp_to_vlesstls # cloud
        mode tcp
        server reality1 /x-ui/vlesstls.sock send-proxy tfo #check
        retry-on conn-failure empty-response response-timeout

Теперь создадим фейковый сервис:

frontend f_vless # cloud
        bind /var/lib/haproxy/fakevless1.sock allow-0rtt tfo accept-proxy alpn h2,http/1.1 mode 660 user haproxy group haproxy 
        http-request reject if { req.hdr(user-agent) -m sub evil }
        acl url_discovery path /.well-known/caldav /.well-known/carddav
        http-request redirect location /remote.php/dav/ code 301 if url_discovery
        http-request deny if { path -m sub /. }
        stick-table type ip size 100k expire 2m store http_req_rate(10s)
        http-request track-sc0 src
        http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
        http-response set-header X-Content-Type-Options nosniff
        http-response set-header X-XSS-Protection 1;mode=block
        http-response set-header X-Frame-Options SAMEORIGIN
        http-response set-header Strict-Transport-Security "max-age=16000000;" # Устанавливаем HSTS
        default_backend nextcloud

Однако, можно провернуть все тоже самое без дополнительного frontend'а добавив необходимое в основной frontend, приме:

frontend f_https # 
        bind abns@frontendhttps.sock accept-proxy ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 strict-sni tfo
        bind /var/lib/haproxy/fakevless1.sock allow-0rtt tfo accept-proxy alpn h2,http/1.1 mode 660 user haproxy group haproxy
        http-request reject if { req.hdr(user-agent) -m sub evil }
        http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам
        http-response set-header X-Content-Type-Options nosniff
        http-response set-header X-XSS-Protection 1;mode=block
        http-response set-header X-Frame-Options SAMEORIGIN
        http-after-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains;" # Устанавливаем HSTS
        stick-table type ip size 100k expire 5m store http_req_rate(10s)
        http-request track-sc0 src
        http-request deny deny_status 429 if { sc_http_req_rate(0) gt 300 }
        http-request return status 200 content-type text/plain lf-string "Status 200 OK! Your IP %[src]." if { path /https-status-443 } 

        use_backend nextcloud if { req.hdr(host) -i cloud.вашдомен } || { ssl_fc_sni -i вашдомен }  
        use_backend http_adh if { ssl_fc_sni -i вашдомен } { path_beg -i /dns-query/ } 

Спасибо за пост. Напишите пожалуйста область применения.

mTLS для сервисов внутри компании, сотрудник ушел - отозвали. Так же этим методом защищаю админ панели и метрики.
GeoIP как раз применяется для маршрутизации по странам.
DoH что бы раздавать клиентам в OpenVPN приватные домены.
Автопродление сертификатов без перезапуска HAProxy через acme и deploy метод.
С 3xui я думаю понятно.

Это если они нужны ) а если пользователи только браузера и по другому не умеют, то им оно и не нужно, прикрываем лозейку и все.

ну и curl и wget юзерагент могут в ключах командной строки указать.

Так как решение не для открытого интернета, в целях тестирования чего-либо указываем кастомный UA который не будет блокироваться, и все

Дополнение к статье

VLESS TCP TLS

РКН начали ломать TLS, что бы обойти можно открыть диапазон портов, пример для секции f_tcp:

frontend f_tcp
        bind [::]:443 v4v6 # SSL Passthrough
        bind [::]:4000-9000 v4v6   # SSL Passthrough extended_1 ports // ufw allow 4000:9000/tcp
        bind [::]:15000-20000 v4v6 # SSL Passthrough extended_2 ports // ufw allow 15000:20000/tcp
        bind [::]:63000-65534 v4v6 # SSL Passthrough extended_4 ports // ufw allow 63000:65534/tcp

Пока Сибирь и ДВ жаловались на проблемы с REALITY, VLESS TLS нужно тестировать.

Создадим скрипт который будет мониторить обновление сертификата через сокет haproxy:

import subprocess
import re
import time
import os

# Путь к файлу для хранения даты сертификата
CERT_DATA_FILE = '/tmp/certdata.txt'
# Путь к сокету HAProxy
SOCKET_PATH = '/var/run/haproxy/admin.sock'
# Команда для получения информации о сертификате
COMMAND = f'echo "show ssl cert /etc/haproxy/certs/$вашдомен.pem" | nc -U {SOCKET_PATH}'
# Период проверки в секундах
CHECK_INTERVAL = 25

def get_cert_not_before():
    """Получает дату 'notBefore' из сертификата."""
    try:
        result = subprocess.check_output(COMMAND, shell=True, text=True)
        match = re.search(r'notBefore:\s*(.*)', result)
        if match:
            return match.group(1).strip()
    except subprocess.CalledProcessError as e:
        print(f"Error executing command: {e}")
    return None

def read_previous_cert_date():
    """Читает предыдущую дату сертификата из файла."""
    if os.path.exists(CERT_DATA_FILE):
        with open(CERT_DATA_FILE, 'r') as f:
            return f.read().strip()
    return None

def write_cert_date(date):
    """Записывает дату сертификата в файл."""
    with open(CERT_DATA_FILE, 'w') as f:
        f.write(date)

def restart_service():
    """Перезапускает сервис x-ui."""
    try:
        subprocess.run(['systemctl', 'restart', 'x-ui'], check=True)
        print("Сервис x-ui перезапущен.")
    except subprocess.CalledProcessError as e:
        print(f"Ошибка при перезапуске сервиса: {e}")

def main():
    """Основная функция для мониторинга сертификата."""
    while True:
        current_cert_date = get_cert_not_before()
        if current_cert_date:
            previous_cert_date = read_previous_cert_date()

            if current_cert_date != previous_cert_date:
                print(f"Дата сертификата изменилась: {previous_cert_date} -> {current_cert_date}")
                write_cert_date(current_cert_date)
                restart_service()
            else:
                print("Дата сертификата не изменилась.")

        time.sleep(CHECK_INTERVAL)

if __name__ == "__main__":
    main()

Теперь сделаем его исполняем chmod 700 certmon.py

Разместим в systemd
nano /etc/systemd/system/certmon.service

[Unit]
Description=Monitor HAProxy SSL Certificate

[Service]
ExecStart=/usr/bin/python3 /path/to/your/certmon.py
Restart=always

[Install]
WantedBy=multi-user.target

Для VLESS TLS необходимо настроить хранилище сертификатов таким же путем как и с haproxy, в директорию /etc/x-ui/ssl
После получим сертификат:

#!/bin/sh
DEPLOY_HAPROXY_HOT_UPDATE=yes \
DEPLOY_HAPROXY_STATS_SOCKET=/var/run/haproxy/admin.sock \
DEPLOY_HAPROXY_PEM_PATH=/etc/haproxy/certs \
acme.sh --issue -d $ВАШДОМЕН --stateless && \
acme.sh --install-cert -d $ВАШДОМЕН --cert-file /etc/x-ui/ssl/cert.pem \
--key-file /etc/x-ui/ssl/cert.key --fullchain-file /etc/x-ui/ssl/fullchain.ru.pem && \
acme.sh --deploy -d $ВАШДОМЕН --deploy-hook haproxy

Подождем и зайдем в панель, и настроим vless tls
Обязательно указываем в inbound, мониторинг IP /var/lib/haproxy/xui/vlesstls.sock,0660
Proxy Protocol = true
Sockopt = true
TCP Fast Open = true
Multipath TCP = true
External Proxy = true
Тот же / cloud.вашдомен / 443
Безопасность TLS 
SNI cloud.вашдомен
uTLS на ваш выбор
ALPN h2 http/1.1
И укажем путь к сертификату
Публичный ключ: /etc/x-ui/ssl/cert.pem
Закрытый ключ: /etc/x-ui/ssl/cert.key.
Теперь обязательно создадим fallback для маскировки,
В настройках inbound VLESS TLS добавляем fallback, sni alpn path оставляем пустыми, в Dest указываем /var/lib/haproxy/fakevless1.sock и xVer устанавливаем в 1.

Теперь настроим haproxy, настроим фронтенд tcp для отправки трафика на x-ui:

use_backend tcp_to_vlesstls if { req.ssl_sni -i cloud.вашдомен }

Теперь настроим backend:

backend tcp_to_vlesstls # cloud
        mode tcp
        server reality1 /x-ui/vlesstls.sock send-proxy tfo #check
        retry-on conn-failure empty-response response-timeout

Теперь создадим фейковый сервис:

frontend f_vless # cloud
        bind /var/lib/haproxy/fakevless1.sock allow-0rtt tfo accept-proxy alpn h2,http/1.1 mode 660 user haproxy group haproxy 
        http-request reject if { req.hdr(user-agent) -m sub evil }
        acl url_discovery path /.well-known/caldav /.well-known/carddav
        http-request redirect location /remote.php/dav/ code 301 if url_discovery
        http-request deny if { path -m sub /. }
        stick-table type ip size 100k expire 2m store http_req_rate(10s)
        http-request track-sc0 src
        http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
        http-response set-header X-Content-Type-Options nosniff
        http-response set-header X-XSS-Protection 1;mode=block
        http-response set-header X-Frame-Options SAMEORIGIN
        http-response set-header Strict-Transport-Security "max-age=16000000;" # Устанавливаем HSTS
        default_backend nextcloud

Однако, можно провернуть все тоже самое без дополнительного frontend'а добавив необходимое в основной frontend, приме:

frontend f_https # 
        bind abns@frontendhttps.sock accept-proxy ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 strict-sni tfo
        bind /var/lib/haproxy/fakevless1.sock allow-0rtt tfo accept-proxy alpn h2,http/1.1 mode 660 user haproxy group haproxy
        http-request reject if { req.hdr(user-agent) -m sub evil }
        http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам
        http-response set-header X-Content-Type-Options nosniff
        http-response set-header X-XSS-Protection 1;mode=block
        http-response set-header X-Frame-Options SAMEORIGIN
        http-after-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains;" # Устанавливаем HSTS
        stick-table type ip size 100k expire 5m store http_req_rate(10s)
        http-request track-sc0 src
        http-request deny deny_status 429 if { sc_http_req_rate(0) gt 300 }
        http-request return status 200 content-type text/plain lf-string "Status 200 OK! Your IP %[src]." if { path /https-status-443 } 

        use_backend nextcloud if { req.hdr(host) -i cloud.вашдомен } || { ssl_fc_sni -i вашдомен }  
        use_backend http_adh if { ssl_fc_sni -i вашдомен } { path_beg -i /dns-query/ } 
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации