Первая Часть

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>

Схема запросов будет выглядеть примерно так:

Рис. 1
Рис. 1

Плюсы и минусы DNS балансировки

Данную балансировку я использовал довольно длительное время.

Плюсы:

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

  • Для добавления/удаления новых серверов достаточно добавить/удалить A запись у совего DNS провайдера.

Минусы:

  • При добавлении нового сервера может быть большой временной лаг, так как нужно некоторое время, прежде чем DNS серверы подтянут к себе обновленную информацию.

  • При удалении сервера необходимо сначала удалить DNS запись и только через некоторое время можно потушить сервер, так как не все DNS серверы успеют убрать запись о старом IP адресе.

  • Если у нас падает какой‑то из серверов, DNS запись не обновится автоматически, и некоторая часть пользователей будет пытаться подсоединиться к серверу, который не работает

  • Балансировка не происходит оптимальным образом, так как используется алгоритм Round Robin.

Про получение пользователем конфигурации

рис. 2
рис. 2

Таблица 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

  1. Пользователь делает запрос в TelegramBot для получения конфигурации.

  2. TelegramBot получает chat_id пользователя. Далее он генерирует public_key, private_key, wireguard_ip и добавляет все эти данные в таблицу users. Также TelegramBot делает следующую запись в таблице actions:

id

action

user_id

timestamp

1

1 (подключить пользователя)

<user_uuid>

<Текущее время>

  1. Создается конфигурация пользователя и возвращется пользователю в виде .conf файла.

  2. Slave1Worker и Slave2Worker каждые 5 секунд делают запрос в Master на получение свежих записей из таблицы actions. Если action = 1, то ключи пользователя добавляются в Wireguard, если action = 2, то ключи пользователя удаляются.

Безопасность

Так как master и slave хосты отправляют запросы по IP адресу (неудобно использовать доменные имена, так как по доменному имени у нас реализована балансировка), нету возможности использовать SSL. Из‑за этого возникает 2 уязвимости связанные с Man‑in‑the‑middle attack.

  1. Передаваемые между master и slave данные возможно прочесть (злоумышленник может украсть ключи пользователя и использовать VPN вместо него).

  2. Запросы между master и slave можно перехватить и потом отправить повторно. Так как API master и slave не является идемпотентным, то возможно изменить внутреннее состояние системы и вызвать ошибки.

Было приниято решение все запросы шифровать с помощью RSA ключа. Для этого создаются пары приватный/публичный ключ. Выглядит это так:

рис. 3
рис. 3
  1. Запрос кодируется с помощью master_private_key

  2. Запрос уходит во внешнюю сеть

  3. Запрос приходит в slave хост

  4. Запрос декодируется с помощью master_public_key

  5. Формируется ответ

  6. Ответ кодируется с помощью slave_private_key

  7. Ответ уходит во внешнюю сеть

  8. Master получает ответ

  9. Ответ декодируется с помощью slave_public_key и обрабатывается в responseHandler

Шифрование запросов между серверами делает невозможным прочесть передаваемые данные. Для того чтобы защититься от повторного отправления запроса, было решено добавлять в запрос timestamp. В таком случае, при получении запроса можно проверять, когда был подписан запрос, и, если подпись старая, то отклонять такие запросы. Я решил использовать дедлайн для подписи в 5 секунд. Этого оказалось достаточным для того, чтобы запросы успевали доходить до адресата.

Далее будет еще одна статья, в которой я расскажу подробнее про организацию кода в своем проекте, github actions, про делегацию балансировки клиентам и про то, какие есть планы на будущее.

Комментарии (12)


  1. maximw
    00.00.0000 00:00
    +1

    А почему нет возможности использовать SSL? Из-за использования голого ip адреса? Быстрый гуглеж привел к ответу, что можно https://stackoverflow.com/questions/1095780/are-ssl-certificates-bound-to-the-servers-ip-address

    Накрайняк можно self-signed использовать.


    1. AotD
      00.00.0000 00:00
      +1

      Или почему бы для master и slave серверов не использовать всё тот же DNS?

      A - master.domainName.com - master ip
      A - slave.domainName.com - slave ip
      A - vpn.domainName.com - worker ip 1
      A - vpn.domainName.com - worker ip 2
      A - vpn.domainName.com - worker ip 3
      ...


    1. ky0
      00.00.0000 00:00
      +3

      Третью часть автор наверняка начнёт так — «в прошлой части кто-то спросил, почему нельзя использовать SSL, выпустив самоподписанные или wildcard-сертификаты»…


      1. tarmalonchik Автор
        00.00.0000 00:00

        Нет, не начну)


    1. tarmalonchik Автор
      00.00.0000 00:00

      Спасибо за наводку. Не знал про возможность использовать SSL напрямую с IP адресом


    1. fk0
      00.00.0000 00:00
      -1

      Да там вообще SSL не нужен. Т.е. нужен, но на другом уровне: в VPN-сети которая свяжет все сервера между собой. А на уровне приложения -- не нужен.


    1. serega404
      00.00.0000 00:00

      Кратко: wireguard не может работать с ssl

      Полно: при атаке на используемый пользователем DNS, можно подменить домен, а вот сертификат нельзя, но wireguard не может использовать ssl, как минимум из-за того, что работает по другому протоколу


  1. beduin01
    00.00.0000 00:00
    +2

    Расскажите а как как организован эквайринг? Как платежи принимаете от клиентов?


  1. fk0
    00.00.0000 00:00
    -1

    Так как master и slave хосты отправляют запросы по IP адресу (неудобно использовать доменные имена, так как по доменному имени у нас реализована балансировка), нету возможности использовать SSL...

    Ну так один IP можно вписать в несколько разных имён. Пусть, условно, provider.net указывает на 1.2.3.4 и 5.6.7.8, а alpha.provider.net только на 1.2.3.4, а beta.provider.net только на 5.6.7.8.

    Да, нужно использовать поддомены.


  1. fk0
    00.00.0000 00:00
    -1

    Не понимаю зачем телеграм-бот. То же самое можно сделать на обычном веб-сайте CGI-скриптами. И не нужно регистрироваться в телеграмме.


  1. fk0
    00.00.0000 00:00
    -2

    Создается конфигурация пользователя и возвращется пользователю в виде .conf файла.

    А вот можно этот момент поподробней. Я правильно понял, что приватный ключ пользователя генерируется вашей системой и даётся пользователю в телеграм чате. Где Паша Дуров записывает этот ключ себе в блокнотик...


  1. fk0
    00.00.0000 00:00
    -1

    Шифрование запросов между серверами делает невозможным прочесть передаваемые данные. Для того чтобы защититься от повторного отправления запроса, было решено добавлять в запрос timestamp...

    Какой ужас. Между серверами нужно было протянуть VPN попросту. С использованием того же WireGuard хотя бы. Да хотя бы autossh наконец!!!