├── .env.template ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── README_RU.md ├── config_fetcher.lua ├── docker-compose.yml └── nginx.conf.esh /.env.template: -------------------------------------------------------------------------------- 1 | # Nginx 2 | PATH_SSL_KEY=/etc/letsencrypt/live/your_site/ 3 | SITE_HOST=subserver.example 4 | SITE_PORT=443 5 | SERVERS="https://server1.com/sub/ https://server2.com/sub/" 6 | SUB=sub 7 | TLS_MODE=off 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | /htmlcov/ 4 | */venv/ 5 | */.venv/ 6 | static/ 7 | testy-static/ 8 | media/ 9 | /pg-data/ 10 | /redis_data/ 11 | .env 12 | .env.local 13 | db.sqlite3 14 | .DS_Store 15 | .idea 16 | .coverage 17 | .vscode 18 | .pytest_cache 19 | grafana-data 20 | loki-data 21 | node_modules/ 22 | testy_static/dist/*.html 23 | *.egg-info/ 24 | *.egg 25 | volumes/ 26 | nginx/*.crt 27 | nginx/*.key 28 | /.env.prod 29 | /.gitlab-ci.yml 30 | /.gitleaks.toml 31 | /.gitleaksignore 32 | /backend/user_activity.log 33 | celerybeat-schedule 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Используем OpenResty на базе Alpine 2 | FROM openresty/openresty:alpine-fat 3 | 4 | ARG SITE_PORT=443 5 | ARG SERVERS 6 | 7 | # Прокидываем переменные 8 | ENV SITE_HOST=localhost 9 | ENV SITE_PORT=${SITE_PORT} 10 | ENV SERVERS=${SERVERS} 11 | ENV SUB=sub 12 | ENV TLS_MODE=off 13 | 14 | # Выставляем порты 15 | EXPOSE ${SITE_PORT}/tcp 16 | 17 | # Копируем конфигурацию Nginx 18 | RUN rm /usr/local/openresty/nginx/conf/nginx.conf 19 | COPY nginx.conf.esh /usr/local/openresty/nginx/conf/ 20 | 21 | # Устанавливаем esh 22 | RUN apk upgrade && apk add --no-cache \ 23 | esh 24 | 25 | # Устанавливаем Lua-библиотеку resty-http 26 | RUN luarocks install lua-resty-http 27 | 28 | # Копируем конфигурацию Lua 29 | COPY config_fetcher.lua /etc/nginx/lua/ 30 | 31 | # Устанавливаем права на файлы 32 | RUN chmod -R 755 /usr/local/openresty/nginx/conf/ 33 | 34 | # Запускаем nginx со своей конфигурацией 35 | CMD ["/bin/sh", "-c", "esh -o /usr/local/openresty/nginx/conf/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf.esh && exec nginx -g 'daemon off;'"] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Na 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nginx-3x-ui-subscription-proxy 2 | 3 | 🇷🇺 [Русская версия README](README_RU.md) 4 | 5 | A reverse proxy configuration for Nginx to dynamically handle and aggregate [3x-UI](https://github.com/MHSanaei/3x-ui?tab=readme-ov-file) subscriptions from multiple servers. 6 | 7 | Конфигурация Nginx для объединения подписок с нескольких серверов [3x-UI](https://github.com/MHSanaei/3x-ui?tab=readme-ov-file). 8 | 9 | ### Flow 10 | [![Flow](https://i.postimg.cc/pX59gV8h/temp-Image1-Z8b-SK.avif)](https://postimg.cc/8jDPvSZN) 11 | 12 | ## Overview 13 | This project allows you to set up an Nginx-based reverse proxy that fetches and aggregates subscription configurations from multiple 3x-UI servers. It simplifies subscription management by unifying configurations in a single endpoint. 14 | 15 | ## Important Notes 16 | 17 | 1. Each client must have the same **subscription ID** across all your servers. 18 | 2. Subscription encryption must be enabled on all 3x-UI servers. 19 | 20 | 21 | ## Setup Instructions 22 | 23 | ### 1. Clone the Repository 24 | ```bash 25 | git clone https://github.com/apa4h/nginx-3x-ui-subscription-proxy.git 26 | cd nginx-3x-ui-subscription-proxy 27 | ``` 28 | 29 | ### 2. Copy the Environment File 30 | ```bash 31 | cp .env.template .env 32 | ``` 33 | 34 | ### 3. Configure Environment Variables 35 | Edit the `.env` file and fill in the following variables with your own data: 36 | 37 | | Variable | Description | 38 | |-----------------|-------------------------------------------------------------------------------------------------| 39 | | `TLS_MODE` | Enables or disables SSL. Default set `off`. When set to `on`, SSL certificates must be generated (e.g., via Certbot), and their paths must be specified in the `PATH_SSL_KEY` variable. | 40 | | `PATH_SSL_KEY` | Path to the directory containing your SSL certificate and private key (e.g., `/etc/letsencrypt/live/your_site/`). | 41 | | `SITE_HOST` | Domain name for your Nginx server (e.g., `subserver.example`). | 42 | | `SITE_PORT` | Port number where Nginx will listen for requests (e.g., `443`). | 43 | | `SERVERS` | List of 3x-UI server URLs to aggregate subscriptions from (e.g., `https://server1.com/sub/ https://server2.com/sub/`). | 44 | | `SUB` | Static part of the subscription path (e.g., `sub`). | 45 | 46 | #### Subscription URL Format 47 | 48 | Once you've configured the environment variables, your subscription URL will look like this: 49 | `https://subserver.example/sub/subscription_ID` 50 | 51 | Where: 52 | - `subserver.example` is the domain you set in the `SITE_HOST` variable. 53 | - `sub` is the static part of the subscription path, set in the `SUB` variable. 54 | - `subscription_ID` is the unique ID for each client from 3x-ui. 55 | 56 | ### 4. Start the Application 57 | Run the following command to start the application: 58 | ```bash 59 | docker compose up -d 60 | ``` 61 | 62 | This will build and start the Nginx container with the provided configuration. 63 | 64 | ## How It Works 65 | - The proxy dynamically fetches subscription configurations from the servers listed in `SERVERS`. 66 | - It listens on the domain and port specified in `SITE_HOST` and `SITE_PORT`. 67 | - SSL certificates are loaded from the path specified in `PATH_SSL_KEY`. 68 | 69 | ## Example Configuration 70 | Here is an example `.env` file: 71 | ```dotenv 72 | PATH_SSL_KEY=/etc/letsencrypt/live/example.com/ 73 | SITE_HOST=example.com 74 | SITE_PORT=443 75 | SERVERS="https://server1.com/sub/ https://server2.com/sub/" 76 | SUB=sub 77 | TLS_MODE=off 78 | ``` 79 | 80 | ## License 81 | This project is licensed under the MIT License. See the `LICENSE` file for details. 82 | 83 | ## Contributing 84 | Contributions are welcome! Feel free to open an issue or submit a pull request. -------------------------------------------------------------------------------- /README_RU.md: -------------------------------------------------------------------------------- 1 | # nginx-3x-ui-subscription-proxy 2 | 3 | Конфигурация обратного прокси-сервера Nginx для объединения подписок с нескольких серверов [3x-UI](https://github.com/MHSanaei/3x-ui?tab=readme-ov-file) в одну. 4 | 5 | ### Схема работы 6 | 7 | [![Схема работы](https://i.postimg.cc/pX59gV8h/temp-Image1-Z8b-SK.avif)](https://postimg.cc/8jDPvSZN) 8 | 9 | ## Описание 10 | 11 | Этот проект позволяет настроить Nginx в качестве обратного прокси, который получает и агрегирует подписки с нескольких серверов 3x-UI. Это упрощает использование, например когда у вас несколько серверов в разных гео, будет только одна точка входа. 12 | 13 | ## Важные замечания 14 | 15 | 1. У каждого клиента должен быть одинаковый **subscription ID** на всех ваших серверах 3x-UI. 16 | 2. Шифрование подписки должно быть включено на всех серверах 3x-UI. 17 | 18 | ## Инструкция по установке 19 | 20 | ### 1. Клонирование репозитория 21 | 22 | ```bash 23 | git clone https://github.com/apa4h/nginx-3x-ui-subscription-proxy.git 24 | cd nginx-3x-ui-subscription-proxy 25 | ``` 26 | 27 | ### 2. Копирование файла конфигурации окружения 28 | 29 | ```bash 30 | cp .env.template .env 31 | ``` 32 | 33 | ### 3. Настройка переменных окружения 34 | 35 | Откройте файл `.env` и укажите в нём следующие параметры: 36 | 37 | | Переменная | Описание | 38 | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | 39 | | `TLS_MODE` | Включает или отключает SSL. По умолчанию `off`. Если `on`, необходимо сгенерировать SSL-сертификаты (например, через Certbot) и указать пути в `PATH_SSL_KEY`. | 40 | | `PATH_SSL_KEY` | Путь к директории, содержащей SSL-сертификаты и приватный ключ (например, `/etc/letsencrypt/live/your_site/`). | 41 | | `SITE_HOST` | Доменное имя для вашего сервера Nginx (например, `subserver.example`). | 42 | | `SITE_PORT` | Порт, на котором Nginx будет принимать запросы (например, `443`). | 43 | | `SERVERS` | Список URL серверов 3x-UI, с которых будут агрегироваться подписки (например, `https://server1.com/sub/ https://server2.com/sub/`). | 44 | | `SUB` | Статическая часть пути подписки для прокси сервера (например, `sub`). | 45 | 46 | #### Формат ссылки подписки 47 | 48 | После настройки переменных окружения ваша ссылка на подписку будет выглядеть так: 49 | 50 | ```sh 51 | https://subserver.example/sub/subscription_ID 52 | ``` 53 | 54 | Где: 55 | 56 | - `subserver.example` — домен, указанный в переменной `SITE_HOST`. 57 | - `sub` — статическая часть пути подписки, заданная в `SUB`. 58 | - `subscription_ID` — уникальный идентификатор подписки клиента в 3x-UI. 59 | 60 | ### 4. Запуск 61 | 62 | Запустите приложение командой: 63 | 64 | ```bash 65 | docker compose up -d 66 | ``` 67 | 68 | Это создаст и запустит контейнер Nginx с нужной конфигурацией. 69 | 70 | ## Как это работает 71 | 72 | - Прокси получает конфигурации подписок с серверов, перечисленных в `SERVERS`, объединяет и отдает клиенту все одной зашифрованной подпиской. 73 | - Слушает запросы на домене и порте, указанном в `SITE_HOST` и `SITE_PORT`. 74 | - Если `TLS_MODE=on`, то используются SSL-сертификаты, хранящиеся в `PATH_SSL_KEY`. 75 | 76 | ## Пример файла конфигурации 77 | 78 | Пример файла `.env`: 79 | 80 | ```dotenv 81 | PATH_SSL_KEY=/etc/letsencrypt/live/example.com/ 82 | SITE_HOST=example.com 83 | SITE_PORT=443 84 | SERVERS="https://server1.com/sub/ https://server2.com/sub/" 85 | SUB=sub 86 | TLS_MODE=off 87 | ``` 88 | 89 | ## Лицензия 90 | 91 | Этот проект распространяется под лицензией MIT. Подробности в файле `LICENSE`. 92 | 93 | ## Внесение изменений 94 | 95 | Приветствуются любые улучшения! Открывайте issue или отправляйте pull request. 96 | -------------------------------------------------------------------------------- /config_fetcher.lua: -------------------------------------------------------------------------------- 1 | local http = require "resty.http" 2 | 3 | -- Получаем список серверов из переменной окружения 4 | local servers_str = os.getenv("SERVERS") 5 | if not servers_str then 6 | ngx.log(ngx.ERR, "No servers found in environment variable") 7 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 8 | end 9 | 10 | -- Разделяем строку на таблицу серверов 11 | local servers = {} 12 | for server in string.gmatch(servers_str, "[^%s]+") do 13 | table.insert(servers, server) 14 | end 15 | 16 | local httpc = http.new() 17 | local configs = {} 18 | 19 | -- Запрашиваем конфигурацию с каждого сервера 20 | for _, base_url in ipairs(servers) do 21 | local url = base_url .. ngx.var.sub_id 22 | local res, err = httpc:request_uri(url, { 23 | method = "GET", 24 | ssl_verify = false, -- Параметр для пропуска проверки SSL-сертификатов (если необходимо) 25 | }) 26 | 27 | if res and res.status == 200 then 28 | -- Декодируем ответ 29 | local decoded_config = ngx.decode_base64(res.body) 30 | if decoded_config then 31 | table.insert(configs, decoded_config) 32 | else 33 | ngx.log(ngx.ERR, "Failed to decode base64 from ", url) 34 | end 35 | else 36 | ngx.log(ngx.ERR, "Error fetching from ", url, ": ", err) 37 | end 38 | end 39 | 40 | -- Возвращаем объединённые конфигурации клиенту 41 | if #configs > 0 then 42 | -- Объединяем без добавления новой строки между конфигурациями 43 | local combined_configs = table.concat(configs) 44 | local encoded_combined_configs = ngx.encode_base64(combined_configs) 45 | ngx.header.content_type = "text/plain; charset=utf-8" -- Устанавливаем Content-Type 46 | ngx.print(encoded_combined_configs) -- Возвращаем клиенту результат без лишней новой строки 47 | else 48 | ngx.status = ngx.HTTP_BAD_GATEWAY 49 | ngx.say("No configs available") 50 | end 51 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | nginx: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | container_name: nginx_proxy_sub 8 | env_file: 9 | - .env 10 | ports: 11 | - ${SITE_PORT}:${SITE_PORT} 12 | volumes: 13 | - ${PATH_SSL_KEY}/fullchain.pem:/etc/nginx/ssl/fullchain.pem 14 | - ${PATH_SSL_KEY}/privkey.pem:/etc/nginx/ssl/privkey.pem 15 | restart: always 16 | -------------------------------------------------------------------------------- /nginx.conf.esh: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | env SERVERS; 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | include mime.types; 9 | default_type application/octet-stream; 10 | 11 | resolver 127.0.0.11 valid=5s ipv6=off; 12 | 13 | server { 14 | # Включение или отключение TLS 15 | <% if [ "$TLS_MODE" != "off" ]; then -%> 16 | listen <%= $SITE_PORT %> ssl; 17 | ssl_certificate /etc/nginx/ssl/fullchain.pem; 18 | ssl_certificate_key /etc/nginx/ssl/privkey.pem; 19 | <% else -%> 20 | listen <%= $SITE_PORT %>; 21 | <% fi; -%> 22 | 23 | server_name <%= $SITE_HOST %>; 24 | 25 | # Обработка запросов с фиксированной частью пути и переменным sub_id 26 | location ~ ^/<%= $SUB %>/([^/]+)/?$ { 27 | # Извлекаем sub_id из URL 28 | set $sub_id $1; 29 | 30 | content_by_lua_file /etc/nginx/lua/config_fetcher.lua; 31 | } 32 | 33 | # Заглушка для других запросов 34 | location / { 35 | return 404; 36 | } 37 | } 38 | } 39 | --------------------------------------------------------------------------------