Блог о прокси
open main menu

Поговорим о транспортах?

/ 8 min read

article in English

Давайте вместе с Вами исследуем тему транспортов в xray-core, так как видимо сухая техническая документация с указанием параметров, не дает общее представление конечному пользователю.

Вообще, транспорты в нашем ядре — это буквально все что угодно, что может эмулировать (или оборачивать) двунаправленное соединение между сервером и клиентом.

TCP-сокеты являются именно такими, но это не обязательно должен быть TCP или даже основанный на TCP. Это должно быть что-то, что передает байты по порядку от клиента к серверу без их потери и наоборот.

После предоставления этого канала поверх него могут работать Vmess, VLESS или Trojan. Или что-то совершенно другое.

Наш первый транспорт

Мы будем использовать некоторые команды Linux, чтобы показать, что могут делать транспорты. Начнем с чего-то простого.

Откройте терминал и введите это:

nc -l localhost 6003  # сервер

и в другом терминале:

nc localhost 6003  # клиент

Напишите несколько строк в одном терминале и наблюдайте, как они появляются в другом. Это работает в обоих направлениях.

Эта команда эквивалентна “TCP транспорту” в нашем ядре. Вы можете заменить хост и порт в команде client, чтобы проверить, открыт ли TCP-порт.

WebSocket

WebSocket — это протокол на основе HTTP, предназначенный для выполнения той же задачи, что и TCP-сокет.

Основное различие заключается в следующем:

  1. WebSocket добавляет большое рукопожатие на основе HTTP в начале соединения. Это полезно для кукисов, аутентификации, обслуживания нескольких “WebSocket-сервисов” под разными HTTP-путями и для обеспечения того, чтобы веб-сайт мог открывать соединения только с собственным источником, а не с случайными хостами. Для целей создания транспортов некоторые из этих функций, особенно маршрутизация на основе пути, очень полезны, но дополнительный обмен данными в начале добавляет дополнительную задержку.

  2. WebSocket не передает байты, а “сообщения”. Для наших целей это означает, что вместо Write("hello world") и отправки hello world (как в TCP), фактически отправляется <frame header>hello world. Для целей создания транспортов этот дизайн является ненужной нагрузкой.

Причина, по которой мы миримся с этими аспектами, заключается в том, что некоторые CDN могут напрямую пересылать WebSocket.

Если nc фактически является TCP-транспортом, то такие инструменты, как websocat или wscat, фактически являются WebSocket-транспортами.

На самом деле, эти инструменты очень полезны для проверки, правильно ли слушает WebSocket на определенном пути:

curl https://example.com  # example.com является действительным HTTP-сервисом...
websocat wss://example.com  # ...но фактически не является WebSocket! Это не работает

Чтобы получить локальный тестовый сервер, как в предыдущем примере с nc, вы можете сделать это:

websocat -s 6003  # сервер
websocat ws://localhost:6003  # клиент

Еще раз, вы можете отправлять случайные строки текста туда и обратно.

Поскольку WebSocket — это просто HTTP/1.1, а HTTP/1.1 — это просто TCP, мы можем даже направить websocat на nc, чтобы вывести начальное рукопожатие:

nc -l localhost 6003  # сервер
websocat ws://localhost:6003  # клиент

Когда вы выполните вторую команду, она фактически выведет:

GET / HTTP/1.1
Host: localhost:6003
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: MOIjFT7/cVsCCr95mkpCtg==

Это часть нагрузки WebSocket. Клиент все еще ждет ответа. Давайте также извлечем ответ.

Прекратите обе команды с помощью Ctrl-C и запустите websocat -s 6003 как сервер.

Возьмите приведенный выше текст и отправьте его напрямую с помощью nc:

echo 'GET / HTTP/1.1
Host: localhost:6003
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: MOIjFT7/cVsCCr95mkpCtg==
' | unix2dos | nc localhost 6003 | cat -v
  1. echo просто выводит данные
  2. unix2dos преобразует окончания строк, поскольку HTTP/1.1 ожидает \r\n вместо \n
  3. nc отправляет данные на сервер и выводит ответ
  4. cat -v делает специальные символы (или, скорее, байты, не являющиеся символами) видимыми.

Вы увидите ответ:

HTTP/1.1 101 Switching Protocols^M
Sec-WebSocket-Accept: HKN6nOSb0JT0jWhszYuKJPUPpHg=^M
Connection: Upgrade^M
Upgrade: websocket^M

После этого вы можете отправлять данные с сервера на клиент, вводя их в окне терминала сервера.

Введите hello в сервер websocat, и наблюдайте на клиенте:

M-^A^Fhello

Мусор в начале — это фрейм сообщения WebSocket.

Отправка данных с клиента на сервер не работает, поскольку WebSocket ожидает, что данные будут обернуты в этот фрейм сообщений.

WebSocket 0-RTT

(избавляемся от накладных расходов на рукопожатие)

Где TCP просто выполняет свое рукопожатие, WebSocket отправляет HTTP-запрос и ждет ответа. Эта дополнительная задержка очень заметна, особенно в следующем потоке:

  1. Выполнить TCP рукопожатие
  2. Отправить HTTP-запрос
  3. Ждать HTTP-ответа и прочитать его
  4. Отправить инструкцию VLESS для подключения к какому-то веб-сайту pornhub.com
  5. Ждать ответа и прочитать его

Отправьте немного данных, подождите, отправьте еще немного данных, снова подождите.

Было бы быстрее отправить много данных, а затем подождать много данных:

  1. Выполнить TCP рукопожатие (его не избежать… пока…)
  2. Отправить HTTP-запрос вместе с первой инструкцией VLESS для подключения к pornhub.com
  3. Ждать HTTP-ответа и первых байтов тела одновременно

Если мы сможем это достичь, это будет на один RTT меньше.

Xray и Sing-Box реализуют эту идею под названием Early Data или иногда “0-RTT”. Ранние данные — это просто любые данные, которые клиент хочет записать на шаге 2.

  • Sing-Box по умолчанию отправляет это как часть URL, но может быть настроен на использование любого имени заголовка (значение в base64).

  • В Xray это всегда отправляется как заголовок Sec-WebSocket-Protocol. Существует довольно неочевидная причина для этого под названием Browser Dialer для этого конкретного выбора заголовка.

Можно сказать, что отправка этих данных в URL более совместима с HTTP-проксами, но заголовки имеют большую емкость для больших данных.

Это в основном решает проблемы задержки, связанные с WebSocket.

HTTPUpgrade

(избавляемся от накладных расходов на фреймирование данных)

Помните? Когда вы отправляете hello в WebSocket, фактически передается:

M-^A^Fhello

Что за мусор впереди? Это просто трата полосы пропускания. Мы ранее говорили, что стандарт WebSocket этого требует.

Но на самом деле оказывается, что многие CDN не заботятся о том, если вы отправляете фактические данные WebSocket через соединение WebSocket. После первого HTTP-запроса и HTTP-ответа, кажется, что сокет можно использовать напрямую без каких-либо накладных расходов вообще.

Вся идея HTTPUpgrade заключается в этом. Изначально этот вид транспорта был разработан Tor под названием HTTPT, некоторое время после того, как v2ray добавил WebSocket. Позже v2fly перенес его как новый транспорт, затем он был скопирован в Xray.

Таким образом, передача становится такой. От клиента:

GET / HTTP/1.1
Host: localhost:6003
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: MOIjFT7/cVsCCr95mkpCtg==

Затем ответ от сервера:

HTTP/1.1 101 Switching Protocols
Sec-WebSocket-Accept: HKN6nOSb0JT0jWhszYuKJPUPpHg=
Connection: Upgrade
Upgrade: websocket

…и затем он используется напрямую как обычное TCP-соединение, как в первом примере с nc.

HTTPUpgrade 0-RTT

(избавляемся от обеих проблем)

Это работает так же, как WebSocket 0-RTT. Теперь обе проблемы WebSocket устранены: Накладные расходы на рукопожатие (в основном) и накладные расходы на сообщения (полностью).

GRPC

TODO

Перерыв: Смена методов обучения

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

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

Установите mitmproxy для следующих шагов.

После этого давайте попробуем посмотреть на некоторые запросы WebSocket. Выполните эти команды в разных терминалах:

  1. websocat -s 3002
  2. mitmproxy --mode reverse:http://localhost:3002 -p 3001
  3. websocat ws://localhost:3001

Вместо того чтобы websocat общался напрямую с websocat, он общается с mitmproxy, и mitmproxy логирует и пересылает весь трафик.

Как и раньше, вы можете вводить сообщения с обеих сторон. В окне mitmproxy вы можете нажать Enter, чтобы просмотреть HTTP-запрос, затем нажать Tab, чтобы переключиться на вкладку WebSocket Messages. Нажмите q, чтобы вернуться к списку запросов, и q, затем y, чтобы выйти из инспектора.

Теперь давайте посмотрим на SplitHTTP, просто потому что он основан на HTTP и является хорошей целью для mitmproxy.

SplitHTTP

Командой было сделано все возможное, чтобы дать объяснение на высоком уровне в официальной документации Xray, поэтому я даже не собираюсь объяснять, как работает этот протокол, просто расскажу, как перехватывать его трафик и смотреть, что он делает.

Не существует такого инструмента, как websocat для SplitHTTP, но на самом деле вы можете создать такой инструмент с помощью xray. Другими словами, вы можете использовать транспорты Xray без VLESS или Vmess. Просто сырые данные.

Для следующих шагов вам нужно скачать xray. Ядро, без какого-либо графического интерфейса. Перейдите на Xray Releases, и, скорее всего, вам понадобится Xray-linux-64.zip или Xray-linux-arm64-v8a.zip.

Сохраните как client.json:

{
  "log": {
    "access": "/dev/stdout",
    "error": "/dev/stdout",
    "loglevel": "debug"
  },
  "inbounds": [
    {
      "tag": "dkd",
      "listen": "127.0.0.1",
      "port": 5999,
      "protocol": "dokodemo-door",
      "settings": {
        "address": "127.0.0.1",
        "port": 6000,
        "network": "tcp"
      }
    }
  ],
  "outbounds": [
    {
      "protocol": "freedom",
      "streamSettings": {
        "network": "splithttp"
      }
    }
  ]
}

Сохраните как server.json:

{
  "log": {
    "access": "/dev/stdout",
    "error": "/dev/stdout",
    "loglevel": "debug"
  },
  "inbounds": [
    {
      "tag": "dkd",
      "listen": "127.0.0.1",
      "port": 6001,
      "protocol": "dokodemo-door",
      "settings": {
        "address": "127.0.0.1",
        "port": 6002,
        "network": "tcp"
      },
      "streamSettings": {
        "network": "splithttp"
      }
    }
  ],
  "outbounds": [
    {"protocol": "freedom"}
  ]
}

Запустите все снова в разных терминалах:

  1. xray -c client.json
  2. xray -c server.json
  3. mitmproxy --mode reverse:http://localhost:6001 -p 6000 --set stream_large_bodies=0m
  4. nc -l 6002
  5. nc localhost 5999

Теперь поток трафика клиент-сервер выглядит так:

(nc localhost 5999) -> порт 5999 (клиент xray) -> порт 6000 (mitmproxy)
-> порт 6001 (сервер xray) -> nc -l 6002)

В этой настройке xray (клиент) действует как port forward, но при переадресации трафика он кодируется как splithttp. Затем в mitmproxy вы можете посмотреть, как работает SplitHTTP. В xray (сервер) SplitHTTP снова декодируется, и неупакованный контент пересылается на nc -l 6002.

Введите hello в nc localhost 5999, нажмите клавишу Enter и посмотрите, что происходит в mitmproxy. Клиент xray должен отправить GET и POST запросы. Перейдите к POST запросу с помощью клавиш со стрелками, и вы должны увидеть Content-Length: 6. Это hello плюс новая строка.

Обратите внимание, что запрос GET еще не завершен (в крайнем правом столбце нет времени ответа). Все, что вы вводите в nc -l 6002, будет передаваться через этот нескончаемый HTTP-ответ. Если вы завершите команды nc, запрос GET завершится.

К сожалению, из-за опции stream_large_bodies все тела запросов и ответов кажутся отсутствующими в mitmproxy, по крайней мере, на моей машине.

TODO