Два DNS-имени, один порт OpenVPN: что на самом деле выбирает
Два имени хоста, указывающие на один адрес, всё равно приходят к одному слушателю. Выбрать бэкенд может только поле, которое отправляет клиент, а обычный OpenVPN такого не отправляет.
Две записи A, один адрес, один демон: какое бы имя ни ввёл пользователь, приходящий пакет одинаков, и у процесса, привязанного к порту 1194, нет ничего, чем можно было бы их различить. Это не ожидающая исправления ошибка конфигурации; это предел того, что может выразить поиск по имени. Всё, что выбирает между двумя экземплярами OpenVPN, должно быть значением, которое клиент кладёт в соединение и которое внешняя часть может прочитать обратно, а стандартный клиент OpenVPN такого значения открыто не кладёт. Далее — к чему это приводит и какие схемы действительно разделяют трафик.
Начните с того, что возвращает распознаватель
Клиент запрашивает vpn-a.example и получает адрес; этот адрес плюс порт — всё, к чему он подключается. Имя расходуется локально, во время поиска, и никогда не становится полем на проводе, поэтому слушатель на той стороне не может увидеть, какая метка использовалась. Направьте vpn-a.example и vpn-b.example на один хост, и оба поиска дадут один и тот же адрес назначения, поэтому демон поглощает оба соединения в один экземпляр, не фиксируя разницы. Профили на основе адреса ведут себя точно так же, как на основе имени, по той же причине.
Это также закрывает семейство заманчивых обходных путей: дополнительные псевдонимы, выбор сертификата для каждого имени на одном сокете или правило брандмауэра, привязанное к запрошенному имени. Ничто из этого не может действовать на информацию, которая уже была отброшена до того, как первый пакет покинул клиента. Если два сервера должны оставаться раздельными, разделение должно путешествовать вместе с самим соединением, то есть адрес, порт или поле полезной нагрузки, которое определяет протокол.
Единственный считающийся селектор едет внутри пакета
HTTP решает это заголовком Host, а TLS — расширением имени сервера в ClientHello, и так один адрес обслуживает множество сайтов за одним слушателем. Оба работают потому, что протокол на проводе несёт запрос к конкретной службе. Существенный факт об OpenVPN состоит в том, что ни одного из этих полей нет на уровне, который просматривает промежуточный узел.
В обычной схеме рукопожатие TLS, защищающее канал управления, происходит после установления внешней сессии и переносится внутри неё, поэтому слушатель, не завершивший эту сессию, видит непрозрачный поток. Он всё ещё может сказать, что это OpenVPN, потому что код операции и кадрирование в первых байтах узнаваемы, но имя сервера, если оно есть, находится в зашифрованной части. Проект, который рассчитывает прочитать виртуальный хост из внешнего пакета, читает поле, которое протокол туда никогда не пишет. Это следует из того, как построен туннель, а не из пробела, который мог бы закрыть более хитрый подбор шаблона.
Где разделитель протоколов останавливается
Мультиплексор того типа, который документирует проект sslh, слушает на одном порту, просматривает первые байты и передаёт соединение тому бэкенду, который говорит на распознанном протоколе. Это реальная ценность, когда один порт должен обслуживать SSH, HTTPS и OpenVPN вместе.
sslh держит отдельный зонд для каждого протокола, и его зонд TLS может вытащить имя сервера или значение ALPN из настоящего ClientHello. Распознавание обычного OpenVPN — это другая классификация, чем чтение расширения TLS, и они не складываются, потому что клиент, который никогда не отправляет ClientHello на слушающий сокет, оставляет зонд без ничего для чтения. Поэтому правило, совпадающее с OpenVPN, отвечает «это клиент OpenVPN» и дальше не идёт. Два бэкенда OpenVPN за этим правилом неразличимы, у внешней части нет оснований для выбора, и каждая сессия приземляется в одном и том же месте. Мультиплексирование делит один порт между разными службами; оно не делает виртуального хостинга внутри одной службы.
Схемы, которые действительно разделяют два сервера
Самое дешёвое честное разделение — второй публичный адрес, по одному на экземпляр, и существующие профили уже несут нужное, потому что указывают на разные адреса. Следующее — другой порт на том же адресе, что не стоит ничего, кроме числа в каждом профиле. TCP и UDP ведут отдельные таблицы портов, поэтому 1194/tcp и 1194/udp — несвязанные сокеты, и по одному из каждого могут сосуществовать на одном хосте; это разделение транспорта, а не маршрутизация по имени, и с ней это часто путают. Оба варианта проходят проверку брандмауэра, потому что правило совпадает с тем, что пакет действительно содержит.
Если нужная вам граница проходит между группами пользователей, а не между двумя реализациями, её может закрыть один экземпляр с разными конфигурациями клиентов, собственной обработкой сертификатов и явной политикой доступа. Это покупает политику внутри сервера, а не изоляцию между серверами, и это разные требования. Изоляция, которая должна выдерживать при компрометации, по-прежнему требует адреса или края брандмауэра.
Терминатор TLS, поставленный перед OpenVPN, может показать настоящее имя сервера способной внешней части, но только когда клиентская сторона тоже настроена на этот дополнительный слой. Существующие профили не приобретают такого поведения просто от указания на тот же адрес.
Докажите селектор до того, как строить
Назовите поле, которое будет идентифицировать бэкенд, затем подтвердите, что клиент действительно его отправляет; если ответ — адрес, план держится, а если это имя сервера внутри рукопожатия, которое никогда не доходит до слушателя, никакая работа с DNS его не спасёт. Три команды быстро решают вопрос слушающей стороны.
ss -lntup | grep -E ':443|:1194'
openssl s_client -connect vpn-a.example:443 -servername vpn-a.example </dev/null | head -n 20
journalctl -u openvpn-server@server-a -n 20 --no-pager
Строка openssl даёт сертификат только там, где что-то на этом адресе завершает TLS; против обычного слушателя OpenVPN она застревает или терпит неудачу, и это само по себе ответ на вопрос, что раскрывает внешний слой. Строка journal, прочитанная на каждом бэкенде, показывает, какой экземпляр принял сессию. Журналы внешней части записывают то, что было классифицировано, и никогда то, что подразумевалось, поэтому бэкенд остаётся единственной инстанцией, которая знает, где приземлился пользователь.
Завершите двумя профилями, предназначенными для разных серверов, подключите их по одному и запишите журнал бэкенда для каждого. Если обе сессии появятся в одном и том же журнале, селектор отсутствует, а не настроен неправильно, и исправление — это адрес или порт, на который клиент может нацелиться. Редактирование правила маршрутизации не поможет, потому что правилу никогда не давали ничего, по чему маршрутизировать. Как только разделение станет реальным, запишите его рядом с профилями, чтобы следующий человек не добавил второй псевдоним и не ждал, что тот сделает работу.