Поговорим о транспортах?
Давайте вместе с Вами исследуем тему транспортов в 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вместо\nncотправляет данные на сервер и выводит ответ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 3002mitmproxy --mode reverse:http://localhost:3002 -p 3001websocat 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.jsonxray -c server.jsonmitmproxy --mode reverse:http://localhost:6001 -p 6000 --set stream_large_bodies=0mnc -l 6002nc 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