Поговорим о транспортах?
Давайте вместе с Вами исследуем тему транспортов в xray-core, так как видимо сухая техническая документация с указанием параметров, не дает общее представление конечному пользователю.
Вообще, транспорты в нашем ядре — это буквально все что угодно, что может эмулировать (или оборачивать) двунаправленное соединение между сервером и клиентом.
TCP-сокеты являются именно такими, но это не обязательно должен быть TCP или даже основанный на TCP. Это должно быть что-то, что передает байты по порядку от клиента к серверу без их потери и наоборот.
После предоставления этого канала поверх него могут работать Vmess, VLESS или Trojan. Или что-то совершенно другое.
Наш первый транспорт
Мы будем использовать некоторые команды Linux, чтобы показать, что могут делать транспорты. Начнем с чего-то простого.
Откройте терминал и введите это:
nc -l localhost 6003 # сервер
и в другом терминале:
nc localhost 6003 # клиент
Напишите несколько строк в одном терминале и наблюдайте, как они появляются в другом. Это работает в обоих направлениях.
Эта команда эквивалентна “TCP транспорту” в нашем ядре. Вы можете заменить хост и порт в команде client
, чтобы проверить, открыт ли TCP-порт.
WebSocket
WebSocket — это протокол на основе HTTP, предназначенный для выполнения той же задачи, что и TCP-сокет.
Основное различие заключается в следующем:
-
WebSocket добавляет большое рукопожатие на основе HTTP в начале соединения. Это полезно для кукисов, аутентификации, обслуживания нескольких “WebSocket-сервисов” под разными HTTP-путями и для обеспечения того, чтобы веб-сайт мог открывать соединения только с собственным источником, а не с случайными хостами. Для целей создания транспортов некоторые из этих функций, особенно маршрутизация на основе пути, очень полезны, но дополнительный обмен данными в начале добавляет дополнительную задержку.
-
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
echo
просто выводит данныеunix2dos
преобразует окончания строк, поскольку HTTP/1.1 ожидает\r\n
вместо\n
nc
отправляет данные на сервер и выводит ответ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-запрос и ждет ответа. Эта дополнительная задержка очень заметна, особенно в следующем потоке:
- Выполнить TCP рукопожатие
- Отправить HTTP-запрос
- Ждать HTTP-ответа и прочитать его
- Отправить инструкцию VLESS для подключения к какому-то веб-сайту
pornhub.com
- Ждать ответа и прочитать его
Отправьте немного данных, подождите, отправьте еще немного данных, снова подождите.
Было бы быстрее отправить много данных, а затем подождать много данных:
- Выполнить TCP рукопожатие (его не избежать… пока…)
- Отправить HTTP-запрос вместе с первой инструкцией VLESS для подключения к
pornhub.com
- Ждать 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. Выполните эти команды в разных терминалах:
websocat -s 3002
mitmproxy --mode reverse:http://localhost:3002 -p 3001
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"}
]
}
Запустите все снова в разных терминалах:
xray -c client.json
xray -c server.json
mitmproxy --mode reverse:http://localhost:6001 -p 6000 --set stream_large_bodies=0m
nc -l 6002
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