Создай свой VPN с нуля: История и практика технологии WARP


Сетевые возможности Linux — ключевой компонент того, как Cloudflare обслуживает миллиарды запросов в условиях DDoS-атак. Предоставляемые им инструменты бесценны и полезны, а постоянный вклад разработчиков со всего мира гарантирует, что он постоянно становится более функциональным и производительным.

Когда мы разрабатывали WARP, наше мобильное приложение для производительности и безопасности, перед нами встала новая задача: как безопасно и эффективно направлять произвольные пользовательские пакеты от миллионов мобильных клиентов с наших пограничных серверов. В этой статье рассматривается наше первое решение, которое по сути заключалось в создании собственного высокопроизводительного VPN на основе сетевого стека Linux. Нам нужно было интегрировать его в существующую сеть; не просто напрямую связать с нашей CDN-службой, но и обеспечить безопасный вывод произвольных пользовательских пакетов с машин Cloudflare. Уроки, извлеченные здесь, помогли нам разработать новые продукты и возможности, а также обнаружить множество других интересных вещей. Но сначала — как мы начали?

Мост между двумя мирами

Первоначальная реализация WARP напоминала виртуальную частную сеть (VPN), обеспечивающую доступ в Интернет. Конкретно — VPN уровня 3, туннель для IP-пакетов.

IP-пакеты — это строительные блоки Интернета. При отправке данных через Интернет они разбиваются на небольшие фрагменты и отправляются отдельно в пакетах, каждый из которых помечен адресом назначения (кому предназначен пакет) и исходным адресом (кому отправлять ответ). Если вы подключены к Интернету, у вас есть IP-адрес.

Однако у вас может не быть уникального IP-адреса. Это особенно верно для IPv4, который, несмотря на наши и многие другие долгосрочные усилия по переходу на IPv6, все еще широко используется. В IPv4 всего 4 миллиарда возможных адресов, и все они уже распределены — придется делиться.

При использовании Wi-Fi дома, на работе или в кафе вы подключаетесь к локальной сети. Вашему устройству назначается локальный IP-адрес для связи с точкой доступа и другими устройствами в вашей сети. Однако этот адрес не имеет значения за пределами локальной сети. Вы не можете использовать его в IP-пакетах, отправляемых через Интернет, потому что каждая локальная сеть IPv4 использует одни и те же наборы адресов.

Как тогда работает доступ в Интернет? Локальные сети IPv4 обычно используют маршрутизатор — устройство, выполняющее преобразование сетевых адресов (NAT). NAT используется для преобразования частных адресов IPv4, выделенных устройствам в локальной сети, в небольшой набор публичных маршрутизируемых адресов, предоставленных вашим интернет-провайдером. Маршрутизатор отслеживает преобразования между двумя сетями в таблице трансляции. При получении пакета в любой из сетей маршрутизатор обращается к таблице трансляции и применяет соответствующее преобразование перед отправкой пакета в противоположную сеть.


Диаграмма маршрутизатора, использующего NAT для соединения устройств в частной сети с публичным Интернетом

VPN, предоставляющая доступ в Интернет, в этом отношении ничем не отличается от локальной сети — необычен лишь тот факт, что пользователь VPN связывается с VPN-сервером через публичный Интернет. Модель проста: IP-пакеты частной сети инкапсулируются в публичные IP-пакеты, адресованные VPN-серверу.


Схема инкапсуляции HTTPS-пакетов между VPN-клиентом и сервером

Чаще всего VPN-программное обеспечение обрабатывает только инкапсуляцию и декапсуляцию пакетов, предоставляя виртуальное сетевое устройство для отправки и получения пакетов через VPN. Это дает свободу настройки VPN по своему усмотрению. Для WARP нам нужно, чтобы наши серверы действовали как маршрутизаторы между VPN-клиентом и Интернетом.

NAT — вот как это делается

Linux — операционная система, работающая на наших серверах — можно настроить для выполнения маршрутизации с NAT в подсистеме Netfilter. Netfilter часто настраивается с помощью правил nftables или iptables. Настройка «source NAT» для перезаписи исходного IP исходящих пакетов достигается одним правилом:

nft add rule ip nat postrouting oifname "eth0" ip saddr 10.0.0.0/8 snat to 198.51.100.42

Это правило настраивает функцию NAT в Netfilter для выполнения преобразования исходного адреса для любого пакета, соответствующего следующим критериям:

  1. Исходный адрес принадлежит подсети частной сети 10.0.0.0/8 — в этом примере предположим, что VPN-клиенты используют адреса из этой подсети.

  2. Пакет должен быть отправлен через интерфейс «eth0» — в этом примере это единственный физический сетевой интерфейс сервера, а значит, маршрут в публичный Интернет.

При выполнении этих двух условий мы применяем действие «snat» для перезаписи исходного IP-пакета, с любого адреса, используемого VPN-клиентом, на публичный IP-адрес нашего сервера 198.51.100.42. Мы отслеживаем исходные и измененные адреса в таблице преобразования.

Создай свой VPN с нуля: История и практика технологии WARP

Схема декапсуляции и перезаписи инкапсулированного пакета VPN-сервером

Может потребоваться дополнительная настройка в зависимости от того, как ваша система поставляет nftables — nftables гибче устаревшего iptables, но имеет меньше «неявных» таблиц, готовых к использованию.

Также может понадобиться включить IP-маршрутизацию в целом, поскольку по умолчанию нежелательно, чтобы машина, подключенная к двум разным сетям, перенаправляла трафик между ними без ведома администратора.

Conntrack — он и в Африке conntrack

Ранее мы говорили, что маршрутизатор отслеживает преобразования между адресами в двух сетях. На диаграмме выше это состояние хранится в таблице преобразования.

На практике любое устройство может эффективно реализовать NAT, только если понимает протоколы TCP и UDP, в частности, как они используют номера портов для поддержки множества независимых потоков данных на одном IP-адресе. Устройство NAT — в нашем случае Linux — гарантирует, что для каждого соединения используется уникальный исходный порт и адрес, и при необходимости переназначает порт. Оно также должно понимать жизненный цикл TCP-соединения, чтобы знать, когда безопасно повторно использовать номер порта: при всего 65 536 возможных портах их повторное использование необходимо.

В Linux Netfilter есть модуль conntrack, широко используемый для реализации stateful firewall, защищающего серверы от поддельных или неожиданных пакетов, предотвращая их вмешательство в легитимные соединения. Такая защита возможна, потому что он понимает TCP и допустимое состояние соединения. Эта возможность также идеально подходит для реализации NAT. Фактически, вся перезапись пакетов реализована через conntrack.


Диаграмма шагов, выполняемых conntrack для проверки и перезаписи пакетов

Как stateful firewall, модуль conntrack поддерживает таблицу всех увиденных им соединений. Если известны все активные соединения, можно переназначить новое соединение на порт, который не используется.

В правиле «snat» выше Netfilter добавляет запись в таблицу преобразования, но еще не изменяет пакет. Только базовые изменения пакета разрешены внутри nftables. Мы должны дождаться, пока обработка пакета достигнет модуля conntrack, который выберет порт, не используемый активными соединениями, и только затем перезапишет пакет.


Диаграмма, показывающая роли netfilter и conntrack при применении NAT к трафику

Метки и фаервол

Другой режим conntrack — назначение постоянной метки пакетам, принадлежащим соединению. Метка может использоваться в правилах nftables для реализации различных политик firewall или управления решениями маршрутизации.

Предположим, вы хотите запретить определенным адресам (например, из гостевой сети) доступ к определенным службам на вашей машине. Вы можете добавить правило firewall для каждой службы, запрещающее доступ с этих адресов. Однако если нужно изменить набор блокируемых адресов, придется обновлять каждое правило.

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

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

Создай свой VPN с нуля: История и практика технологии WARP

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

Код, обеспечивающий работу сервиса WARP, был написан Cloudflare на Rust - системном языке программирования, ориентированном на безопасность. Мы тщательно подошли к реализации boringtun - нашей реализации WireGuard - и MASQUE. Но даже если вы считаете, что входная дверь неприступна, хорошей практикой безопасности является применение глубокой эшелонированной защиты.

Один из примеров - различение IP-пакетов, поступающих от клиентов, и пакетов, исходящих из других частей нашей сети. Распространенный метод - выделение уникального IP-пространства для трафика WARP и его идентификация по IP-адресу, но это может быть ненадежно, если нам потребуется применить изменение конфигурации для перенумерации наших внутренних сетей - помните об ограниченном адресном пространстве IPv4! Вместо этого мы можем сделать нечто более простое.

Для передачи IP-пакетов от клиентов WARP в сетевой стек Linux, WARP использует устройство TUN - так в Linux называется виртуальное сетевое устройство, которое программы могут использовать для отправки и получения IP-пакетов. Устройство TUN можно настраивать аналогично любому другому сетевому устройству, такому как Ethernet или Wi-Fi адаптеры, включая настройку межсетевого экрана и маршрутизации.

Используя nftables, мы помечаем все пакеты, выходящие на устройстве TUN WARP. Мы должны явно сохранять метку в таблице состояний conntrack на исходящем пути и извлекать ее для входящего пакета, поскольку netfilter может использовать метки пакетов независимо от conntrack.

table ip mangle {
    chain forward {
        type filter hook forward priority mangle; policy accept;
        oifname "fishtun" counter ct mark set 42
    }
    chain prerouting {
        type filter hook prerouting priority mangle; policy accept;
        counter meta mark set ct mark
    }
}

Нам также нужно добавить правило маршрутизации для возврата помеченных пакетов на устройство TUN:

ip rule add fwmark 42 table 100 priority 10 ip route add 0.0.0.0/0 proto static dev warp-tun table 100

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

Конец?

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

Linux гибок и прост в настройке, но для этого потребовался бы один IPv4-адрес на машину. Из-за истощения адресного пространства IPv4 этот подход не масштабировался бы до крупной сети Cloudflare. Выделение dedicated IPv4-адреса для каждой машины, на которой работает сервер WARP, привело бы к астрономическому счету за аренду адресов. Чтобы снизить затраты, нам пришлось бы ограничить количество серверов, работающих с WARP, что увеличило бы операционную сложность его развертывания.