DNS Балансировка
Предыдущая часть закончилась неудачной балансировкой, которая не решает практически никаких проблем. В комментариях кто‑то спросил, почему я не использовал балансировку на уровне DNS. Так вот, я ее использовал. Оказалось, что c помощью DNS записей можно организовать балансировку Round Robin. Для этого в конфигурации Wireguard всего лишь нужно использовать доменное имя вместо IP адреса. Теперь конфигурация Wireguard будет выглядеть вот так:
[Interface]
PrivateKey = <client_private_key>
Address = <cient_address_on_server>/32
DNS = 8.8.8.8, 1.1.1.1
[Peer]
PublicKey = <server_private_key>
AllowedIPs = 0.0.0.0/0
Endpoint = domainName.com:<server_port>
Схема запросов будет выглядеть примерно так:
Плюсы и минусы DNS балансировки
Данную балансировку я использовал довольно длительное время.
Плюсы:
-
Нет необходимости иметь свой собственный сервер для балансировки, соответственно, не нужно платить за дополнительный трафик и дополнительные серверы.
-
Для добавления/удаления новых серверов достаточно добавить/удалить A запись у совего DNS провайдера.
Минусы:
-
При добавлении нового сервера может быть большой временной лаг, так как нужно некоторое время, прежде чем DNS серверы подтянут к себе обновленную информацию.
-
При удалении сервера необходимо сначала удалить DNS запись и только через некоторое время можно потушить сервер, так как не все DNS серверы успеют убрать запись о старом IP адресе.
-
Если у нас падает какой‑то из серверов, DNS запись не обновится автоматически, и некоторая часть пользователей будет пытаться подсоединиться к серверу, который не работает
-
Балансировка не происходит оптимальным образом, так как используется алгоритм Round Robin.
Про получение пользователем конфигурации
Таблица actions:
id |
int64 (уникальный идентификатор каждой записи) |
action |
ENUM (1 - подключить пользователя; 2 - отключить пользователя) |
user_id |
uuid (уникальный идентификатор пользователя) |
timestamp |
timestamptz (время создания записи) |
Таблица users:
id |
uuid (уникальный идентификатор пользователя) |
chat_id |
int64 (уникальный id пользователя в Telegram) |
public_key |
text (публичный ключ Wireguard) |
private_key |
text (приватный ключ Wireguard) |
wireguard_ip |
text (уникальный ip адрес каждого пользователя внутри интерфейса Wireguard) |
subcription_end |
timestamptz (время, когда у пользователя кончится подписка) |
Как я уже говорил в прошлой части, у меня есть Master и Slave хосты:
Master включает в себя:
-
TelegramBot — отвечает за взаимодействие с пользователем. Следит за состоянием подписки, принимает платежи от пользователей, регистрирует новых пользователей, возвращает пользователю конфигурацию для подключения к VPN.
-
SlavePingWorker — отвечает за проверку исправности серверов. Каждые несколько минут он пингует все slave. Если slave не отвечает, то отправляется ALERT.
-
PostgresDB — хранит данные пользователей и таблицу actions.
-
Server — отдает slave хостам записи из таблицы actions.
Slave включает в себя:
-
SlaveWorker — отправляет запрос в master для получение свежих записей из таблицы actions.
Рассмотрим шаги на рис. 2
-
Пользователь делает запрос в TelegramBot для получения конфигурации.
-
TelegramBot получает chat_id пользователя. Далее он генерирует public_key, private_key, wireguard_ip и добавляет все эти данные в таблицу users. Также TelegramBot делает следующую запись в таблице actions:
id |
action |
user_id |
timestamp |
1 |
1 (подключить пользователя) |
<user_uuid> |
<Текущее время> |
-
Создается конфигурация пользователя и возвращется пользователю в виде .conf файла.
-
Slave1Worker и Slave2Worker каждые 5 секунд делают запрос в Master на получение свежих записей из таблицы actions. Если action = 1, то ключи пользователя добавляются в Wireguard, если action = 2, то ключи пользователя удаляются.
Безопасность
Так как master и slave хосты отправляют запросы по IP адресу (неудобно использовать доменные имена, так как по доменному имени у нас реализована балансировка), нету возможности использовать SSL. Из‑за этого возникает 2 уязвимости связанные с Man‑in‑the‑middle attack.
-
Передаваемые между master и slave данные возможно прочесть (злоумышленник может украсть ключи пользователя и использовать VPN вместо него).
-
Запросы между master и slave можно перехватить и потом отправить повторно. Так как API master и slave не является идемпотентным, то возможно изменить внутреннее состояние системы и вызвать ошибки.
Было приниято решение все запросы шифровать с помощью RSA ключа. Для этого создаются пары приватный/публичный ключ. Выглядит это так:
-
Запрос кодируется с помощью master_private_key
-
Запрос уходит во внешнюю сеть
-
Запрос приходит в slave хост
-
Запрос декодируется с помощью master_public_key
-
Формируется ответ
-
Ответ кодируется с помощью slave_private_key
-
Ответ уходит во внешнюю сеть
-
Master получает ответ
-
Ответ декодируется с помощью slave_public_key и обрабатывается в responseHandler
Шифрование запросов между серверами делает невозможным прочесть передаваемые данные. Для того чтобы защититься от повторного отправления запроса, было решено добавлять в запрос timestamp. В таком случае, при получении запроса можно проверять, когда был подписан запрос, и, если подпись старая, то отклонять такие запросы. Я решил использовать дедлайн для подписи в 5 секунд. Этого оказалось достаточным для того, чтобы запросы успевали доходить до адресата.
Далее будет еще одна статья, в которой я расскажу подробнее про организацию кода в своем проекте, github actions, про делегацию балансировки клиентам и про то, какие есть планы на будущее.
Автор:
tarmalonchik