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

Особенности UDP в протоколах прокси

/ 12 min read

Большинство пользователей не имеют представления о роли UDP в протоколах прокси. В настоящее время даже опытные пользователи и разработчики сталкиваются с трудностями при работе с UDP, что приводит к тому, что многие проблемы, связанные с UDP, остаются нерешенными. Учитывая возрастающую важность UDP и отсутствие подробных статей о UDP в протоколах прокси, была написана данная работа.

Цель этой статьи - изменить текущую ситуацию и предоставить полное понимание UDP, что позволит более эффективно использовать Xray-core и другое программное обеспечение для проксирования.

Основные концепции: IP-пакет, TCP-соединение, пятиэлементные наборы, порты, User Datagram Protocol

Это фундаментальные концепции, которые необходимо освоить. На самом деле они достаточно просты.

IP-пакет

IP-пакет представляет собой отдельную единицу данных, соответствующую протоколу IP. Характеристики IP-пакетов включают:

  1. Допустимость потери пакетов
  2. Возможность изменения порядка доставки (т.е. порядок приема может отличаться от порядка отправки)
  3. Ненадежность передачи

IP-пакет не является самым низкоуровневым элементом, и здесь нет необходимости углубляться в детали. Важно понимать его основные характеристики. IP-пакеты не могут обеспечить надежную передачу данных, что неудобно для приложений. Поэтому был разработан протокол TCP, ориентированный на установление соединения и основанный на IP-пакетах, но реализующий механизм надежной передачи. Большинство других протоколов могут быть реализованы поверх TCP.

TCP-соединение

TCP-соединение определяется “пятиэлементным набором”:

  1. Идентификатор протокола TCP
  2. IP-адрес отправителя
  3. Порт отправителя
  4. IP-адрес получателя
  5. Порт получателя

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

Концепция “порта” широко известна и часто упоминается вместе с IP, но на самом деле она не относится к протоколу IP. Порты принадлежат к протоколам более высокого уровня, таким как TCP и UDP. TCP и UDP реализуют механизм “портов”, поэтому порты этих двух протоколов не влияют друг на друга. Протокол, используемый командой ping, - ICMP, он также основан на IP-пакетах и похож на TCP и UDP, но у ICMP нет концепции “порта”. Стоит отметить, что обычные протоколы прокси могут проксировать только TCP или добавлять поддержку UDP, но не могут проксировать ICMP, поэтому ping через них невозможен. Для этого необходимо использовать традиционный VPN.

UDP (User Datagram Protocol)

UDP, как и TCP, основан на IP-пакетах, но является чрезвычайно простым протоколом, полностью наследующим характеристики IP-пакетов:

  1. Допустимость потери пакетов
  2. Возможность изменения порядка доставки
  3. Ненадежность передачи

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

В UDP отсутствует механизм “соединения”, характерный для TCP. После получения локального UDP-порта можно напрямую отправлять прикладные данные на любой UDP-порт любого IP-адреса без необходимости установления соединения. При этом нет необходимости заботиться о том, получила ли другая сторона данные, и другая сторона также не подтверждает получение данных. (Примечание: существует промежуточное состояние “connected UDP”, которое просто определяет адрес назначения перед отправкой данных, без реального установления соединения)

Эти характеристики UDP определили три основных сценария его применения:

  1. Повышение эффективности, например, для запросов DNS (не требуется предварительное установление соединения)
  2. Обеспечение работы в реальном времени, например, для прямых трансляций и передачи голосовых данных (допускается потеря пакетов, не требуется ожидание повторной отправки)
  3. Комбинация двух предыдущих аспектов + поддержка P2P, например, для некоторых онлайн-игр и передачи голосовых данных. Это полное использование вышеупомянутых особенностей UDP, что является ключевым моментом данной статьи.

Существует еще один способ использования UDP: создание новых универсальных надежных транспортных протоколов на его основе, таких как KCP и QUIC. Почему эти новые протоколы основаны на UDP, а не напрямую на IP? Потому что прямая реализация на уровне IP часто требует от операторов изменения оборудования и систем на всех уровнях для обеспечения поддержки, что не всегда реалистично. Поэтому UDP стал более подходящим выбором для создания новых протоколов.

Что такое FullCone и Symmetric NAT?

Эти два термина относятся к поведению NAT (Network Address Translation), то есть преобразованию сетевых адресов, которое выполняется домашними маршрутизаторами и провайдерами на различных уровнях. Широкое распространение NAT обусловлено нехваткой адресов IPv4, а также тем, что он может обеспечивать защиту устройств в локальной сети.

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

Однако для UDP поведение NAT имеет решающее значение, так как UDP представляет собой пакеты с неопределенным направлением, и часто используется один и тот же локальный UDP-порт для отправки пакетов различным целевым двухэлементным наборам.

Двухэлементный набор состоит из IP-адреса и порта. Любое отличие в этих элементах рассматривается как различные двухэлементные наборы.

Как маршрутизатор должен перенаправлять UDP-пакеты при их получении? Это зависит от типа NAT, реализованного в маршрутизаторе.

Существует множество видов поведения NAT, но рассмотрим наиболее показательные случаи. Представим следующие сценарии:

  1. Локальный исходный двухэлементный набор A отправляет несколько пакетов удаленному целевому двухэлементному набору M.
  2. Локальный исходный двухэлементный набор A отправляет несколько пакетов другому удаленному целевому двухэлементному набору N.

Если из-за различия целевых двухэлементных наборов маршрутизатор отображает A->M и A->N как A1->M и A2->N соответственно (обычно используя два разных порта для отправки пакетов) и строго ограничивает источник обратных пакетов, это относится к Symmetric NAT. Нетрудно заметить, что в этом случае коммуникация становится похожей на TCP-”соединение”, что является практикой большинства провайдеров.

Если маршрутизатор рассматривает только исходный двухэлементный набор A и всегда отображает его как свой A1 для отправки пакетов к M и N, это относится к Cone NAT. Далее, если A1 получает обратный пакет, и маршрутизатор отправляет его обратно к A, не обращая внимания на источник, это относится к FullCone NAT. FullCone является наиболее благоприятным типом NAT для программного обеспечения прокси и необходимым атрибутом для P2P-игр (например, “NAT открыт” в GTA).

Приведенные выше примеры упрощены. На практике провайдеры редко предоставляют публичный IP-адрес напрямую, что означает, что прохождение через несколько уровней NAT и получение Symmetric NAT является нормой. Почему же использование протоколов прокси, таких как Shadowsocks или Trojan от Xray-core, позволяет получить FullCone NAT?

Это происходит потому, что в данном случае используется публичный IP-адрес вашего VPS, который не связан с вашей локальной средой NAT.

Вот почему важно специально настроить брандмауэр VPS: по умолчанию он будет фильтровать источник возвращаемых пакетов, что приведет к тому, что вы получите какой-то вид Restricted Cone NAT, а не FullCone NAT.

Для простых потребностей UDP, таких как запросы DNS, Symmetric NAT также может быть использован. Но для более сложных требований к UDP, таких как различные P2P-сценарии, реализация FullCone NAT становится критически важной. Приложению необходимо использовать фиксированный порт для отправки пакетов к любой цели и получения пакетов от любого источника без ограничений (по крайней мере, не должно быть ошибок в определении текущего типа NAT, что будет объяснено далее). Если ваша основная цель - игры, вы можете позволить UDP работать через протокол Shadowsocks, поскольку он обладает нативными характеристиками UDP.

Здесь стоит отметить, что Xray-core планирует внедрение протоколов, более подходящих для игр.

Особенности UDP в Xray-core и некоторых прокси-протоколах

Xray-core поддерживает как FullCone, так и Symmetric режимы NAT, и предоставляет очень широкую поддержку протоколов, что делает его идеальным примером для рассмотрения.

Поддержка FullCone NAT реализована для:

  • Shadowsocks (вход и выход)
  • Trojan (вход и выход)
  • Socks (вход и выход)
  • Dokodemo-door TPROXY (вход, прозрачный прокси)
  • Freedom (выход, с поддержкой разрешения доменных имен)

Поддержка только Symmetric NAT реализована для:

  • VMess (структура протокола не поддерживает FullCone)
  • Текущий Mux (также из-за структуры протокола)
  • VLESS (FullCone в разработке)

Известно, что поддержка UDP в v2ray оставляет желать лучшего. Поэтому Xray-core был перестроен с учетом связанных архитектур и кода для входящих и исходящих соединений. После многократных тестов и устранения множества проблем была достигнута полная поддержка FullCone NAT (за исключением случаев, когда протокол не поддерживает его, для которых предусмотрен адекватный Symmetric NAT; у VMess в Clash есть проблемы с реализацией).

В заметках к релизу Xray-core содержится много полезной информации:

  1. Socks5 и Shadowsocks используют нативный UDP, и их UDP-трафик не проходит через нижележащий транспорт.
  2. VLESS, Trojan, VMess, Mux используют UDP over TCP и проходят через нижележащий транспорт.
  3. HTTP на входе и выходе не поддерживает проксирование UDP, Socks до версии 5 также не поддерживает UDP.
  4. FullCone в данном контексте относится к поведению NAT для UDP; при настройке особое внимание следует уделять конфигурации брандмауэра.
  5. Для реализации FullCone NAT в цепочке прокси, как правило, все звенья должны поддерживать FullCone NAT.
  6. Для реализации FullCone NAT в Docker сетевой режим связанных контейнеров должен быть установлен как Host.

Дополнительно отметим:

  • Socks и Shadowsocks используют нативный UDP, и их UDP-трафик не подвергается специальной обработке при использовании TLS/WSS, если только не включен Mux. UDP-плагины SIP003 для Shadowsocks также не управляются.
  • UDP over TCP (сокращенно UoT) важно отличать от нативного UDP. Даже если используются mKCP или QUIC в качестве нижележащего транспорта, UoT не будет демонстрировать те же характеристики, что и нативный UDP.

Проблемы, существующие в v2ray-core, здесь разъясняются, чтобы сделать UDP менее загадочным:

  1. Архитектурно v2ray-core поддерживает только Symmetric NAT, поэтому любой протокол, используемый в v2ray-core, будет работать только в режиме Symmetric NAT.

  2. Обработка UDP в v2ray-core похожа на логику TCP, и невозможно реализовать специфическое для UDP поведение FullCone NAT.​​​​​​​​​​​​​​​​

  3. При обработке входящих пакетов в выходном модуле Freedom v2ray-core не фильтрует источник в соответствии с принципом Symmetric NAT, что является основной причиной “мистического” поведения.

  4. В v2ray-core время неактивности для поддержания отношений маппинга UDP очень короткое, что легко приводит к обрывам потока данных.

Относительно третьего пункта можно сказать следующее:

  1. В нормальной ситуации Symmetric NAT, если A отправил пакеты M, он может получать пакеты только от M, а остальные будут отфильтрованы.
  2. Если между ними вставлен v2ray, даже если A не отправлял пакеты N, A всё равно может получать пакеты от N.
  3. Главное, что адрес N при этом теряется, и A будет считать, что пакеты пришли от M, что является уникальным источником путаницы.

Такое поведение не ожидается, и в сочетании с некоторыми нестандартными тестовыми серверами это может привести к тому, что v2ray и VMess показывают FullCone NAT, который на самом деле не работает.

В конце июля прошлого года этот вопрос обсуждался в группе разработчиков (касательно нестандартных тестовых серверов, таких как серверы Google, и запутанного поведения UDP в v2ray). После этого обновление NatTypeTester оставило только пять стандартных тестовых серверов и специально проверило исходящие адреса пакетов, что привело к отображению результатов для v2ray как UnsupportedServer.

Кроме того, некоторые приложения Google сначала самостоятельно тестируют тип NAT текущей сети, и если обнаруживается ложный FullCone NAT, это может привести к различным странным проблемам.

Действительно, поведение NAT не ограничивается только FullCone и Symmetric NAT, хотя это два крайних случая. Фактически, поведение NAT определяется двумя аспектами: “маппингом при отправке пакетов” и “фильтрацией при получении пакетов”. FullCone NAT является самым открытым в обоих аспектах, в то время как Symmetric NAT является самым строгим.

RFC 3489 определяет четыре классических типа поведения NAT, и v2ray на самом деле не подпадает ни под один из них напрямую. Его поведение наиболее близко к Address and Port-Dependent Mapping с Endpoint-Independent Filtering, что соответствует NAT Type 7 по RFC 5780. Однако v2ray может неправильно обрабатывать источник возвращаемых пакетов.

Эти четыре классических типа включают:

  • Full Cone: Любой внешний хост может отправлять пакеты на внутренний IP-адрес и порт, если внутренний хост уже отправил пакеты наружу.
  • Address Restricted Cone: Внешний хост может отправлять пакеты на внутренний IP-адрес и порт только если внутренний хост уже отправлял пакеты этому внешнему хосту.
  • Port Restricted Cone: Похоже на Address Restricted Cone, но внешний хост должен отправлять пакеты на тот же порт, с которого внутренний хост отправлял пакеты на внешний хост.
  • Symmetric: Внутренний хост использует разные маппинги для каждого внешнего хоста и порта, с которыми он общается, и только пакеты, отправленные на соответствующий внешний адрес и порт, будут приняты.

Как Xray-core реализует протокол прокси с FullCone NAT

Принцип реализации FullCone NAT в Xray-core довольно прост: он отображает один из локальных UDP-портов на UDP-порт VPS, обеспечивая им одинаковое поведение.

Рассмотрим самый простой сценарий: Socks на входе + Freedom на выходе.

  1. Socks на входе получает Socks UDP-пакет от двухэлементного набора A, который содержит оригинальную полезную нагрузку и её исходную цель M, и направляет его в Freedom на выходе.
  2. Freedom на выходе использует случайный порт для отправки оригинальной полезной нагрузки на её исходную цель M. Предположим, что Freedom использует двухэлементный набор A1.
  3. Отношение отображения уже установлено, и в течение некоторого времени, если Socks на входе снова получает прокси-пакет от A, Freedom продолжит отправлять его на цель, используя A1.
  4. Аналогично, если Freedom с A1 получает пакет от N, Socks отправит оригинальную полезную нагрузку и информацию о N обратно к A.

Разумеется, для достижения FullCone NAT вызывающей стороне необходимо правильно использовать протокол прокси Socks, и обычно реализации tun2socks работают без проблем.

Теперь добавим в сценарий Shadowsocks на входе и выходе:

  1. Трафик направляется на выход Shadowsocks, который также использует случайный порт для отправки зашифрованной “полезной нагрузки и цели” на серверный вход Shadowsocks.
  2. Вход Shadowsocks на сервере получает Shadowsocks UDP-пакет от клиентского выхода Shadowsocks, расшифровывает его, и дальнейший процесс идентичен описанному выше.

Протокол Trojan для UDP работает по аналогичному принципу, но каждый исходный двухэлементный набор соответствует одному TCP-соединению, по которому передается “полезная нагрузка и цель” UDP.

Почему же VMess, который также использует UoT, не может реализовать FullCone NAT?

Основная причина заключается в том, что структура протокола UoT VMess позволяет передать “цель” только в самом начале, а последующие пакеты могут передавать только “полезную нагрузку” без “цели”. Сервер будет отправлять все последующие пакеты на исходную “цель”. Сервер отправляет пакеты обратно клиенту по той же логике: структура протокола содержит только “полезную нагрузку”, и клиент будет считать, что все возвращенные пакеты исходят из первоначальной “цели”. Это продолжение проблемы дизайна v2ray, и, конечно, встроенный Mux работает таким же образом. (VLESS также затрагивается, но уже находится в процессе изменений).

Таким образом, для VMess, Mux и VLESS Xray-core в настоящее время использует маршрутизацию по принципу Symmetric NAT. В противном случае, если использовать режим FullCone NAT, последующие пакеты будут отправляться на адрес первого пакета, что является проблемой Clash с VMess, но это сложно исправить. Кроме того, когда такая проблема существует, VMess может быть ошибочно идентифицирован как FullCone NAT, что неверно.

Принцип прозрачного проксирования UDP через TPROXY заключается в следующем: чтобы использовать Xray-core для реализации FullCone NAT для игровых консолей, обычно требуется Linux-устройство для настройки прозрачного прокси, например, Raspberry Pi.

Почему для прозрачного проксирования UDP предпочтительнее использовать TPROXY, а не REDIRECT?

  1. REDIRECT изменяет целевой двухэлементный набор UDP-пакетов, и в этом случае Linux не предоставляет механизм, позволяющий программе-прокси узнать исходный адрес назначения UDP-пакета.
  2. TPROXY работает наоборот: он не изменяет UDP-пакеты, и Linux предоставляет простой механизм, позволяющий программе-прокси узнать исходный адрес назначения UDP-пакета.

Когда необходимо отправить пакет обратно, программное обеспечение-прокси сначала создает локальный сокет, имитирующий “исходный двухэлементный набор источника возвращаемого UDP-пакета”, и использует этот сокет для отправки пакета обратно. Это и есть принцип, из-за которого может возникнуть ошибка “слишком много открытых файлов”. По сравнению с другим программным обеспечением, Xray-core имеет специальные оптимизации для этой ситуации, обеспечивая более элегантное и производительное решение.

Если вы тестируете NAT прозрачного прокси в Windows, обязательно установите текущую сеть как “Частную сеть”. Это распространенная ошибка, на которую многие уже наступили, включая автора.

Отдельно стоит упомянуть о QUIC: когда активирован XTLS в Xray-core, трафик, идущий на UDP-порт 443, по умолчанию блокируется (блокировка QUIC). Это делается для того, чтобы приложения не использовали QUIC, а использовали TLS, благодаря чему XTLS будет действительно работать. На самом деле, QUIC сам по себе не подходит для проксирования, так как он уже включает в себя функции TCP, и использование UoT в этом случае означало бы двойное использование TCP.​​​​​​​​​​​​​​​​