В наше непростое время... Хотя, к чему все это? Все в курсе. Переходим к технической части.

Многие знают, что XRAY умеет получать запрос от клиента на один ip адрес, а отправлять ответ с другого ip адреса. Документации на этот счет в интернете крайне немного (ну или я не умею искать). Есть образцы базовых настроек в примерах на GitHub проекта XTLS и несколько упоминаний о существовании такой фишки в статьях на Хабре без подробностей реализации. Я же попробую описать здесь теорию и настройку разделения входящего и исходящего трафика подробно и с примерами.

Зачем все это нужно?

  1. Теоретически, это усложняет поведенческий анализ трафика со стороны DPI. Паттерны ломаются и, в теории, это помогает обойти блокировки. Но это не точно.

  2. Если такая фича существует, ее нужно запилить и посмотреть как все это работает на практике. Чисто из научного интереса.

На чем будем настраивать?

На сервере стоит Angie (версия 1.11.6) в качестве реверсивного прокси, за ним прячется панель 3x-ui (версия 3.4.0), которая управляет XRAY (версия 26.6.22). Будем исходить из этой конфигурации.

Можно ли реализовать разделение трафика без реверсивного прокси?

Все упирается в расшифровку двух разных потоков SSL в рамках одного входящего подключения в XRAY. Умеет он такое или нет, я информации не нашел. В настройках подключения есть возможность указать несколько SSL сертификатов, но нет возможности указать несколько SNI, так что возможно, это будет работать только для ip без доменных имен. Скажу честно, я не пробовал.

Важная деталь: у Вашего сервера обязательно должно быть два внешних ip адреса. Желательно из разных подсетей, хотя различие подсетей не играет никакой роли. Большинство хостеров раздает несколько ip на один VDS. У меня эта опция стоит 5 руб в день за каждый дополнительный адрес. Можно делать настройки напрямую с ip адресами, но, для красоты и наглядности, предлагаю к каждому ip привязать свой поддомен. У нас это будут upload.domain.com и download.domain.com. Привязка делается в настройках DNS зоны у регистратора доменов. Интересное наблюдение. Несмотря на то, что оба моих ip адреса на одном и том же VDS, выданы одним и тем же хостером и находятся физически в одном и том же датацентре, 2ip.ru показывает, что один ip адрес расположен в Москве, а второй в Новосибирске, что забавно.

Можно использовать разные VDS для разделения трафика. Физически это могут быть разные машины в разных дата-центрах в разных странах на разных континентах. Для этого нужно с проксировать весь входящий трафик с download сервера на upload сервер с помощью любого реверсивного прокси. На download сервере даже XRAY не нужен. О том как запилить такое проксирование есть ролик на замедленном Youtube на примере Caddy. Но тут есть несколько моментов.

  1. Если оба сервера находятся внутри локальной сети в одном дата-центре, то это ничем не отличается от варианта два ip на одном VDS, а если нет разницы, зачем платить больше?

  2. Если оба сервера находятся в разных дата-центрах, мы получаем дополнительные накладные расходы в виде задержек трафика между серверами без видимой целесообразности, ибо DPI ломается от различия ip адресов, а не от их географического расположения. Опять же, два сервера минимум в два раза дороже, чем один сервер с двумя ip адресами.

Поэтому из экономии будем рассматривать вариант один сервер - два ip.

Итак, погнали. Настраиваем Angie.

Создаем файл xray.conf в папке /etc/angie/http.d. Имя файла может быть любым. Расширение строго conf

xray.conf
server {
    listen x.x.x.1:443 ssl;
    server_name upload.domain.com;

    http2 on;

    ssl_certificate /etc/letsencrypt/live/upload.domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/upload.domain.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets on; 

    client_header_timeout 5m;
    keepalive_timeout 5m;

   location /api/v1/data/ {
        client_max_body_size 0;
        
        proxy_pass http://unix:/dev/shm/xrxhd.socket;
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_cache off;
        
        proxy_connect_timeout 60s;
        proxy_read_timeout 315s;
        proxy_send_timeout 5m;
   }
}

server {
    listen x.x.x.2:443 ssl;
    server_name download.domain.com;

    http2 on;

    ssl_certificate /etc/letsencrypt/live/download.domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/download.domain.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets on; 

    client_header_timeout 5m;
    keepalive_timeout 5m;

   location /api/v1/data/ {
        client_max_body_size 0;
        
        proxy_pass http://unix:/dev/shm/xrxhd.socket;
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_cache off;
        
        proxy_connect_timeout 60s;
        proxy_read_timeout 315s;
        proxy_send_timeout 5m;
   }
}

Пояснения к настройкам:

  1. Для NGINX настройки аналогичные.

  2. Вместо x.x.x.1 и x.x.x.2 пишем внешние ip адреса вашего сервера. Если вы не привязывали доменные имена, поле server_name опускаете (вообще не пишете).

  3. Сертификаты для доменов или ip адресов предварительно нужно получить самостоятельно у Let's Encript или кого угодно еще.

  4. ssl_ciphers и add_header - не догма, пишите как считаете нужным.

  5. Разделы location должны быть аналогичные для обоих ip адресов, включая путь /api/v1/data/ (любой, но одинаковый. И не забудьте слеш в конце) и файл сокета.

  6. Если не хотите использовать сокет, для общения с XRAY, в proxy_pass можно прописать адрес и порт: 127.0.0.1:4443. Порт может быть любым свободным, он дальше будет использоваться при настройке XRAY.

Создаем входящее подключение для XRAY

Через панель 3x-ui: Входящие - Создать подключение.

Вкладка Основное:

Примечание: все что угодно. Это название подключения.

Протокол: vless

Адрес: /dev/shm/xrxhd.socket,0666. Здесь внимательно. Адрес сокета должен совпадать с адресом сокета в настройках реверсивного прокси. После запятой идут права доступа к файлу сокета, их лучше оставить как в примере. Если в настройках реверс прокси вы указывали 127.0.0.1:4443, то здесь пишем 127.0.0.1

Порт. Если используете сокет, ставите 0 или 1, если 127.0.0.1:4443, ставите 4443 (или какой указали в настройках реверсивного прокси).

Вкладка Поток:

Транспорт - XHTTP.

Путь: /api/v1/data - берем из секции location настроек реверсивного прокси. В этом случае без слеша в конце.

Режим: auto - это важно.

Остальное оставляем как есть.

Вкладка Безопасность: выбираем Нет. За расшифровку SSL у нас отвечает реверсивный прокси.

Вкладка Сниффинг: Включить, выбрать все что есть HTTP, TLS, QUIC, FAKEDNS.

Если выше не упомянуты какие-то поля настроек - оставляем их без изменений.

Дальше добавьте клиента в получившееся входящее подключение (раздел панели Клиенты - Добавить клиентов - Привязанные входящие).

В итоге получаем вот такой шаблон входящего подключения:

{
  "listen": "/dev/shm/xrxhd.socket,0666",
  "port": 1,
  "protocol": "vless",
  "tag": "in-1-tcp",
  "settings": {
    "clients": [
      {
        "id": "xxxxxxxxxxxxxxxx",
        "email": "xxxxx",
        "flow": "",
        "limitIp": 0,
        "totalGB": 0,
        "expiryTime": 0,
        "enable": true,
        "tgId": 0,
        "subId": "xxxxxx",
        "comment": "",
        "reset": 0,
        "created_at": 1756652179000,
        "updated_at": 1782274393000
      }
    ],
    "decryption": "none",
    "encryption": "none"
  },
  "sniffing": {
    "enabled": true,
    "destOverride": [
      "http",
      "tls",
      "quic",
      "fakedns"
    ]
  },
  "streamSettings": {
    "network": "xhttp",
    "xhttpSettings": {
      "path": "/api/v1/data",
      "host": "",
      "mode": "auto",
      "xPaddingBytes": "100-1000",
      "scMaxEachPostBytes": "1000000",
      "scMaxBufferedPosts": 30,
      "scStreamUpServerSecs": "20-80"
    },
    "security": "none"
  }
}

На этом с настройками сервера все. Настройка фаервола, маршрутизации в XRAY и прочие настройки на сервере не относятся к делу, а значит выходят за рамки и без того скучной статьи. ИИ в помощь.

Для чистоты эксперимента проверять делится наш трафик или не делится будем на чистом XRAY без графических клиентов. К сторонним клиентам мы еще вернемся. Как известно, бинарник XRAY одновременно является и клиентом и сервером, поэтому качаем последнюю версию под свою платформу (Windows или Linux роли не играет) прямо с GitHub разработчика. Создаем файл конфигурации для клиента config.json.

Примерно такой
{
  "log": {
    "access": "access.log",
    "error": "error.log",
    "loglevel": "debug",
    "dnsLog": true
  },
  "inbounds": [
    {
      "listen": "127.0.0.1",
      "port": 2080,
      "protocol": "socks",
      "settings": {
        "udp": true
      },
      "tag": "socks-in"
    },
    {
      "listen": "127.0.0.1",
      "port": 2081,
      "protocol": "http",
      "tag": "http-in"
    }
  ],
  "outbounds": [
    {
      "protocol": "vless",
      "settings": {
        "vnext": [
          {
            "address": "upload.domain.com",
            "port": 443,
            "users": [
              {
                "id": "xxxxxxxxxxxxx",
                "security": "auto",
                "encryption": "none"
              }
            ]
          }
        ]
      },
      "tag": "vless-out",
      "streamSettings": {
        "network": "xhttp",
        "security": "tls",
        "tlsSettings": {
          "allowInsecure": false,
          "serverName": "upload.domain.com",
          "alpn": [
            "h2",
            "http/1.1"
          ],
          "fingerprint": "firefox"
        },
        "xhttpSettings": {
          "path": "/api/v1/data",
          "host": "upload.domain.com",
          "mode": "auto",
          "xPaddingBytes": "100-1000",
          "downloadSettings": {
            "address": "download.domain.com",
            "port": 443,
            "network": "xhttp",
            "security": "tls",
            "tlsSettings": {
              "serverName": "download.domain.com",
              "alpn": [
                "h2",
                "http/1.1"
              ],
              "fingerprint": "firefox"
            },
            "xhttpSettings": {
              "path": "/api/v1/data",
              "host": "download.domain.com",
              "mode": "auto"
            }
          }
        }
      }
    }
  ]
}

Что важно пояснить в конфигурации.

Во всех секциях, вне “downloadSettings” указываем домен upload.domain.com или ip адрес x.x.x.1 из настроек реверсивного прокси на сервере. Внутри “downloadSettings” соответственно download.domain.com или ip адрес x.x.x.2.

“mode”: “auto” - во всех случаях. Можно в разделе “xhttpSettings”: для исходящего домена upload.domain.com поэкспериментировать с “mode”: “stream-up” или “mode”: “packet-up”, но auto работает точно. “mode”: “stream-one” в нашем случае гарантированно работать не будет. Такова жизнь.

“loglevel”: “debug” - важно указать именно debug, так как нам нужно проверить разделение трафика, а не только ошибки.

Получившийся файл config.json кладем рядом с бинарником XRAY и запускаем из командной строки. Не забудьте сначала в командной строке перейти в папку, где лежит XRAY (cd ... и вот это вот все).

xray --config config.json

Дальше идет описание проверки из под Windows.

Заходим в настройки Windows -> Сеть и Интернет -> Прокси-сервер -> Настройка прокси вручную -> Настройка. Использовать прокси-сервер -> Вкл. IP адрес прокси-сервера 127.0.0.1. Порт 2081 берем из настроек файла клиента (раздел с “protocol”: “http”). Сохраняем, запускаем браузер, открываем 2ip.ru видим ip адрес сервера. Ну или не видим, но чаще видим. Если не видим, проверяем, что сделали не так.

Теперь открываем файл error.log. Он должен появиться в той же папке, что и бинарник XRAY. В этом файле важно найти строки:

transport/internet/splithttp: XHTTP is dialing to tcp:upload.domain.com:443, mode packet-up, HTTP version 2, host upload.domain.com
transport/internet/splithttp: XHTTP is downloading from tcp:download.domain.com:443, mode stream-down, HTTP version 2, host download.domain.com

Должны быть обе строки. Могут идти не подряд, но обязательно должны быть обе. Если в логе есть только первая строка (XHTTP is dialing to tcp ...) - ничего не получилось и весь трафик идет через один ip. Идем в начало статьи, смотрим где накосячили с настройками.

Для тех кто в Linux можно проверить работоспособность нашей поделки через curl. XRAY предварительно должен быть запущен в качестве сервиса или в другом терминале. Номер порта берем из настроек файла клиента (раздел с “protocol”: “socks”).

curl --socks5 127.0.0.1:2080 https://google.com

Дальше, по аналогии с Windows, бежим в логи и ищем две заветные строчки.

Как можно проверить еще, ну вот чтобы вообще на 100%? На примере Windows.

Качаем и запускаем TCPView прямо с сайта Microsoft. В это время у нас уже запущен из командной строки XRAY и включен системный прокси в настройках Windows. В интерфейсе TCPView вверху в поле Search пишем xray, чтобы оставить отображение трафика только одного процеса. Открываем или обновляем в браузере любую страницу. В таблице ниже мы должны увидеть три ip адреса: 127.0.0.1, x.x.x.1 и x.x.x.2. Если видим только два: 127.0.0.1 и x.x.x.1, ничего не работает и день прожит зря. Но, надеюсь, это не про нас.

Что касается графических клиентов для XRAY. Финт с разделением потоков удалось провернуть только на клиенте V2RayN под Windows. Пробовал еще Exclave на Андроиде и Passwall2 на openWRT. В двух последних случаях настройки “downloadSettings” игнорировались и все шло в один поток. Во всех клиентах блок “downloadSettings” я добавлял в раздел "Дополнительные параметры XHTTP (Extra)" в настройках подключения. Просто вставлял блок в соответствующее поле:

"downloadSettings": {
            "address": "download.domain.com",
            "port": 443,
            "network": "xhttp",
            "security": "tls",
            "tlsSettings": {
              "serverName": "download.domain.com",
              "alpn": [
                "h2",
                "http/1.1"
              ],
              "fingerprint": "firefox"
            },
            "xhttpSettings": {
              "path": "/api/v1/data",
              "host": "download.domain.com",
              "mode": "auto"
            }
          }

В случае V2RayN это сработало, Exclave и Passwall2 просто проигнорировали эти настройки, хотя само соединение поднималось. Если у кото-то получится разделить трафик на Андроиде или openWRT, будет очень интересно почитать.

В заключении добавлю, что вся информация предоставлена исключительно в образовательных и научных целях. Ни в коем случае не используйте полученные знания для нарушения закона и доступа к богомерзким запрещенным ресурсам в сети интернет. Да прибудет с Вами сила. Всем бобра.