SysAdmin Anywhere: Используем UDP Hole Punching для реализации удаленного рабочего стола
Системное администрирование*, Облачные вычисления*
Введение
Системные администраторы по своей природе — люди ленивые. Не любят они по сто раз одно и то же делать. Хотят все автоматизировать, чтобы работало с минимальным вмешательством. И я такой.
Дабы облегчить жизнь себе и мне подобным, была создана программа, которая объединила разрозненные средства администрирования в один продукт с простым названием SysAdmin. Он включает поддержку мультидоменной сети, отчеты, инвентаризацию и много других полезных вещей, которые разбросаны по разным MMC или входят в сторонние продукты. Работать стало значительно удобней. Данная программа уже доросла до версии 5.3, сменила интерфейс на Metro-style. Но, к сожалению, в ней нет возможности администрировать компьютеры, не включенные в домен.
Однако, хотелось большего:
Полноценно администрировать удаленные компьютеры;
Настроить доступ без личного присутствия на удаленной стороне и без наличия квалифицированного сотрудника с удаленной стороны;
Не открывать порты на маршрутизаторе;
При невозможности личного присутствия не открывать доступ к маршрутизатору из интернета;
Не усложнять техническое решение, если необходимо иметь доступ не к одному компьютеру в удаленной сети:
Не открывать порты на маршрутизаторе для каждого компьютера;
Не открывать доступ на маршрутизаторе к серверу, а с него к компьютерам (Не всегда есть необходимость в сервере, а введение его в качестве средства для удаленного доступа — экономически нецелесообразно);
Не открывать один порт и каждый раз не перенастраивать маршрутизатор для доступа к другому компьютеру;
Не поднимать VPN.
Из опыта:
По работе надо было создать VPN-сеть с региональными отделениями, расположенными по всей стране. В центральном отделении было установлено мощное VPN-оборудование, а на местах планировалось оборудование той же компании, но проще. Необходимо было выяснить все про их интернет — соединение, локальную сеть, установленное оборудование и т.д. Подготовить VPN-оборудование и отправить им, чтобы они его подключили и все заработало. Пикантность ситуации заключалась в том, что это предстояло сделать фактически мне одному, командировки не предусматривались и уровень пользователей на местах был очень разнообразен. Конечно, все было реализовано, но это отняло массу времени на переписку и телефонные разговоры.
Из-за перечисленных ранее ограничений для реализации был необходим посредник. На удаленный компьютер устанавливается клиентская часть, которая создает соединение с посредником, не блокируемое файерволами, т.к. оно инициировано самой удаленной машиной. Администратору достаточно знать только адрес посредника, чтобы, подключившись к нему, получить доступ к удаленным компьютерам. Благодаря такому посреднику, мы получаем ряд преимуществ:
Нет необходимости открывать порты на маршрутизаторе. Повышается безопасность, снижается уровень необходимой квалификации пользователя;
Администратор имеет доступ к панели управления, расположенной в облаке, через браузер, поддерживающий Microsoft Silverlight посредством защищенного соединения;
Команды от администратора при помощи облачного сервиса отправляются на удаленный компьютер. Ответ передается администратору аналогичным образом;
Удаленный компьютер находится в режиме онлайн, поэтому нет необходимости в присутствии пользователя;
Дополнительные сервисы (инвентаризация, сервис-деск, отчеты, учет лицензий, архивация, мониторинг и т.д.) с хранением данных в облаке и доступа к ним, даже если удаленный компьютер не подключен к интернету;
Удаленный рабочий стол (VNC);
Доступ с мобильного телефона на Windows Phone.
Платформу для посредника долго искать не пришлось. Microsoft Azure, как нельзя кстати, подходила для этого. Тем более была возможность получить 30-ти дневный доступ для экспериментов.
Так сложилось, что к этому времени я сменил сферу деятельности с системного администрирования на программирование., поэтому решил создать сервис самостоятельно.
Ко мне присоединился мой коллега, и мы вместе приступили к разработке прототипа сервиса. Перед нами стояла задача за время тестового периода создать работоспособную модель взаимодействия удаленного клиента с облачным сервисом. Дальнейшие разработки можно было продолжить, используя Azure SDK.
Сложнее всего было организовать постоянное соединение удаленного компьютера с облачным сервисом. В результате долгих и нудных экспериментов (деплой в облако занимает ощутимое время) появилось рабочее решение, которое дало уверенность в достижении поставленной цели.
Мы смогли соединить удаленный компьютер с облаком, при этом ничего не настраивая из сетевого оборудования. Для нас это стало прорывом.
Теперь стояла задача передать команду от администратора в облако, а далее на компьютер и получить от него ответ. Мы остановились на том, что универсальным решением было бы использовать WMI. Причем, облачный сервис не должен знать ничего про WMI. Его задача — пересылка. Это задумывалось для того, чтобы как можно реже приходилось обновлять сам облачный сервис и клиентское ПО на удаленных компьютерах (сейчас обновляется до последней версии нажатием кнопки в консоли).
Пример:
От администратора передается запрос на получение списка установленных служб (Select * From Win32_Service) плюс набор полей, значения которых нам нужны. Все это сериализуется JSON, пакуется, шифруется и передается в облако. В облаке данные записываются в очередь и затем забираются из нее экземпляром службы, к которому подключен удаленный компьютер.
Клиент, получив запрос, выполняет его. Полученный результат сериализуется JSON, пакуется, шифруется и передается в обратном порядке администратору.
Использование WMI не ограничивается только запросом, реализован вызов функций и процедур. Например, любую из служб можно остановить.
Казалось бы, все просто, но не стоит забывать, что если мы планируем сделать масштабируемое решение — без работы с несколькими экземплярами нашего сервиса в облаке не обойтись. Работа с ними требует некоторого пересмотра своего опыта в разработке. Всегда надо держать в голове, что существует не один экземпляр, а несколько, и работают они параллельно. Желаете что-то сделать монопольно — не забывайте оповестить об этом остальные экземпляры. Например, биллинг. Будет крайне неприятно, когда каждый экземпляр пройдется по базе и пересчитает баланс клиента, каждый раз уменьшая его. Такой вариант показан в качестве примера того, что может произойти, хотя в настоящее время мы работаем над тем, чтобы исключить эту проблему в будущем.
Создавая сервис, мы исходили из того, что в большинстве случаев не обязательно иметь доступ к удаленному рабочему столу. Достаточно прямого управления процессами, службами и т.п. Это быстрее и проще сделать через веб-интерфейс.
Все же удаленный рабочий стол очень востребован и для его реализации требуется прямое соединение между компьютерами. Передавать такой объем информации через облако нерационально, так как задержка в обновлении экрана может достигать несколько секунд.
Реализация UDP Hole Punching
Наиболее интересной задачей оказалась реализация системы удаленного доступа к рабочему столу. Для экономии времени было решено взять уже готовую реализацию VNC что, однако, порождало свои проблемы: соединить напрямую клиент и сервер VNC невозможно. Единственным решением явилось создание посредников, задача которых — ретрансляция трафика. Таким образом, VNC клиент и сервер будут “думать”, что работают локально, когда на самом деле весь трафик между ними мы будем передавать через Интернет.
NAT Travesal
Остался открытым вопрос: каким образом передавать информацию между посредниками? Первый приходящий на ум вариант — гонять все через облако — является сколь дорогим столь и неэффективным способом. Гораздо лучше соединить все напрямую. Тут мы и наталкиваемся на стандартную проблему NAT Traversal.
Преобразование сетевых адресов (NAT) не дает возможности устанавливать прямое соединение между клиентами. NAT — это процесс трансляции локальных адресов, недоступных из Интернета, во внешние. Для обхода NAT существует несколько стандартных способов: открыть порт, поднять VPN.
К сожалению, ни один из них нам не подходит, т.к. сразу усложняет установку всех необходимых компонентов (а ведь мы хотим, чтобы установка клиента на рабочей машине была доступна наименее продвинутым пользователям без помощи админа). Поэтому воспользуемся механизмом UDP Hole Punching.
UDP Hole Punching
UDP hole punching — метод для прямого соединения двух компьютеров, которые находятся за NAT-ами. Для инициации соединения требуется третья сторона — сервер, который виден обоим компьютерам. Обычно используются публичные STUN-серверы.
Итак, наша задача состоит в том, чтобы напрямую связать клиента на удаленной машине и консоль. Для этого необходимо сделать несколько шагов:
Узнать внешний IP и порт удаленной машины. Для этого воспользуемся STUN — сетевым протоколом, который позволяет определить внешний IP-адрес.
Передать эту информацию нашему приложению
Установить соединение и использовать его далее для обмена данными
Как это выглядит в коде?
В теории все просто. Как все это выглядит на практике?
Как видно из изображения выше, немногим сложнее. Сначала мы отправляем запрос на соединение от нашего приложения, который через облако передается приложению на клиентской машине (шаги 1 и 2). Клиентская машина запрашивает свой внешний адрес и порт у STUN-сервера (шаги 3 и 4). В сети можно найти множество реализаций STUN протокола.
string address = String.Empty;
udpClient = new UdpClient();
udpClient.AllowNatTraversal(true);
ResultSTUN result = ClientSTUN.Query("stun.ekiga.net", 3478, udpClient.Client);
if (result.NetType == UDP_BLOCKED) { /* обработка ошибки */ }
else { address = result.PublicEndPoint.ToString(); }
Т.к. в нашем решении используется UDP, мы можем, используя тот же сокет, подключиться к клиенту (что выгодно отличает его от TCP).
После этих нехитрых манипуляций address будет содержать внешний адрес и порт клиентской машины (или сообщение об ошибке). Эта ценная информация передается консоли управления через облако (шаги 5 и 6). Создать соединение, зная порт и адрес — уже дело техники. Теперь мы можем передавать всю необходимую нам информацию.
Задача NAT Traversal решается просто и изящно при использовании UDP, но что если мы при этом хотим все преимущества TCP: гарантированную доставку пакетов, целостность данных, отсутствие дублирования и т.д.? Первая мысль, которая приходит в голову — написать свой протокол поверх UDP. Естественно, мы далеко не первые, кто столкнулся с этой проблемой. Поэтому после недолгого поиска можно найти такое решение, как Lindgren.Network, которое в итоге используется в нашем приложении.