Разработка VPN-плагина «Континент-АП» для ОС Sailfish +12


Введение


Я работаю программистом в отделе разработки и тестирования средств защиты мобильных платформ компании «Код безопасности». Перед командой мобильной разработки была поставлена задача портировать кроссплатформенную библиотеку абонентского пункта «Континент-АП», которая уже успешно функционировала на IOS и Android. Основная проблема заключалась в том, что ОС Sailfish не так хорошо документирована, как Android или IOS, но спасибо ребятам из «Открытые Мобильные Платформы», которые поделились документацией.

Архитектура VPN-api в ОС Sailfish


За все сетевые подключения в ОС Sailfish отвечает служба ConnMan, а именно:

  • сканирование Wi-Fi сетей и сетей сотовой связи и подключение к ним;
  • расшаривание подключения (Wi-Fi hotspot);
  • перевод, вывод соединений в режим полета;
  • управление VPN-соединениями.

Про последний пункт я расскажу чуть подробнее. В составе ConnMan есть несколько типов предустановленных плагинов VPN. QML-виджеты для создания и настройки подключения интегрированы в прошивку.

image
Рис. 1 Системное меню ОС Sailfish для настройки и управления VPN-соединениями

UI нашего VPN-клиента представляет из себя RPM-пакет и при установке не встраивается в системное окно VPN в разделе Settings, а выглядит, как отдельное приложение. Про разработку UI, возможно, будет отдельная статья, поэтому следующее повествование будет про разработку ConnMan плагина на С/C++.

image
Рис. 2 Графический интерфейс «Континент-АП» Sailfish

VPN-api ОС Sailfish состоит из следующих компонентов, покажем на примере нашего VPN-клиента:

  1. ConnMan – процесс, запускаемый при старте ОС Sailfish.
  2. connman-vpnd – процесс-демон, запускаемый ConnMan и служащий для управления VPN-соединениями различных провайдеров, инициализацией и деинициализацией tun-интерфейса, назначением на него сетевых настроек (IP-адресов, маршрутов, DNS-серверов), полученных по DBus. В нашем случае провайдер с именем continent.
  3. VPN-плагин continent-proto-plugin.so – библиотека, имеющая макро-объявление для загрузки в runtime и функции, вызываемые при вызове методов интерфейса net.connman.vpn.Connection.
  4. Фоновое приложение (бинарный файл /usr/sbin/continent) – консольный клиент для подключения к СД (Серверу доступа «Континент»), получения от него сетевых настроек, которые он передает в connman-vpnd.
  5. ConnMan Task – процесс для запуска, остановки и контроля запущенного консольного клиента.
  6. DBus-api – представляет connman-vpnd, а именно net.connman.vpn с интерфейсами net.connman.vpn.Manager, net.connman.vpn.Connection.

image
Рис. 3 Взаимодействие компонентов между собой в «Континент-АП» Sailfish

VPN-плагин


Все сторонние плагины, не входящие в дистрибутив ConnMan, представляют библиотеку и должны при установке через пакетный менеджер помещаться в /usr/lib/connman/plugins-vpn/.

Плагин может иметь конфигурационный файл, в котором мы указываем, от какого пользователя запускать бинарный файл, и прописываем его права. Файл должен располагаться в системе по пути /etc/connman/vpn-plugin/continent.conf, а его имя должно соответствовать имени нашего провайдера, в нашем случае continent.conf.

Содержимое файла для примера:

[VPN Binary]
User = nemo
Group = vpn
SupplementaryGroups = inet,net_admin

Плагин continent-proto-plugin.so в ConnMan регистрируется с помощью макроса CONNMAN_PLUGIN_DEFINE (name, description, version, init, exit), в нашем примере вызов макроса будет выглядеть следующим образом:

CONNMAN_PLUGIN_DEFINE(continent,
                "continent VPN plugin",
                CONNMAN_VERSION,
                CONNMAN_PLUGIN_PRIORITY_DEFAULT,
                continent_init,
                continent_exit);

Аргумент name (continent) должен быть без кавычек. Функции continent_init, continent_exit вызываются при загрузке и выгрузке плагина, например, при вызове systemctl restart connman во время инсталляции RPM. Функция continent_init имеет в себе вызов функций vpn_register и connman_dbus_get_connection.

vpn_register(name, driver, binary_path) 

name – имя регистрируемого провайдера, в нашем случае это “continent”;
driver – структура struct vpn_driver, содержащая указатели на функции обратного вызова, например, при обращении к плагину через DBus;
binary_path – путь к бинарному файлу, в нашем случае это “ /usr/sbin/continent”.

Функция connman_dbus_get_connection позволяет получить установленное DBus соединение, DBusConnection *connection.

Функция continent_exit необходима для разрегистрации плагина в ConnMan и закрытия DBus соединение.

Экземпляр VPN-провайдера создается при вызове DBus метода net.connman.vpn.Manager.Create, к нему автоматически создается файл настроек settings в директории /var/lib/connman/provider_${Host}_{VPN.Domain}. Удаляется провайдер вызовом net.connman.vpn.Manager.Remove. При вызове метода net.connman.vpn.Connection.Connect настройки загружаются в созданную struct vpn_provider *provider.

Отдельно хочется рассказать о некоторых функциях в struct vpn_driver, часть из них обязательна для имплементации.

connect – callback, вызываемый при вызове через DBus метода net.connman.vpn.Connection.Connect с соответствующим адресом объекта DBus, имеет сигнатуру:

static int continent_connect(
			struct vpn_provider *provider, 
			struct connman_task *task,
                	const char *if_name, vpn_provider_connect_cb_t cb,
                	const char *dbus_sender, void *user_data)

Вторым аргументом этого callback`а является struct connman_task *task, он и будет запускать бинарный файл, но перед запуском необходимо передать аргументы, для примера – хост и порт сервера:

connman_task_add_argument(task, "--host", value);
connman_task_add_argument(task, "--port", value);

Некоторые параметры мы храним в файле конфигурации экземпляра объекта провайдера, о нем было рассказано выше, и получаем через вызов функции vpn_provider_get_string, например:
char * value = vpn_provider_get_string(provider , “Host”)

где provider – это экземпляр struct vpn_provider.

connman_task_add_argument(task, "--dev-name", if_name).

Строка выше иллюстрирует задание имени виртуального интерфейса, который ConnMan-vpnd инициализирует и предоставляет для чтения и записи IP-пакетов из экземпляра TUN-интерфейса, поднятого для текущего экземпляра VPN-провайдера. В фоновом процессе нам остается открыть устройство и получить файловый дескриптор на чтение/запись.

Небольшое замечание: в процессе разработки плагина выяснилось, что TUN-интерфейс поднимается как интерфейс маршрута по умолчанию.

connman_task_add_argument(task, "--dbus-busname", dbus_bus_get_unique_name(connection));
connman_task_add_argument(task, "--dbus-interface", CONNMAN_TASK_INTERFACE);
connman_task_add_argument(task, "--dbus-path", connman_task_get_path(task));

Для обратной связи фонового приложения и VPN-плагина мы передаем ConnManTask DBus адрес и путь к текущему экземпляру, для этого и потребовалось в функции инициализации вызвать connman_dbus_get_connection.

Запускаем фоновый процесс:

err = connman_task_run(task, continent_died, data, &data->stdin_fd, NULL, NULL);

continent_died – callback, вызываемый когда фоновый процесс завершается. В нем мы узнаем код ошибки завершения процесса, деаллоцируем память, удаляем добавленные маршруты.

notify – callback, вызываемый при вызове через DBus метода net.connman.Task.notify, в нем мы принимаем сообщения DBus от запущенного фонового приложения. Главным является передача сетевых параметров: адрес TUN-интерфейса, DNS сервера в виртуальной сети и т.д. Сетевые параметры упаковываются в DBusMessage в виде словаря и передаются в ConnMan Task, в присланные при запуске фонового приложения Dbus-параметры.

Пример инициализации TUN-интерфейса в функции notify:

struct connman_ipaddress * ipaddress = connman_ipaddress_alloc(AF_INET);
connman_ipaddress_set_ipv4(ipaddress, address, netmask, remote_server_ip);
connman_ipaddress_set_peer(ipaddress, peer);
vpn_provider_set_ipaddress(provider, ipaddress);
vpn_provider_set_nameservers(provider, “8.8.8.8”);
return VPN_STATE_CONNECT;

Мы также передаем промежуточные значения, например, если желаем оповестить UI о событии, записывая в свойства текущего подключения, которые UI может узнать из метода net.connman.vpn.Connection.GetProperties, при изменении Properties ConnMan посылает DBus-сигнал PropertyChanged, например: vpn_provider_set_string(provider, key, value).

disconnect – callback, вызываемый при вызове через DBus метода net.connman.vpn.Connection.Disconnect
Процесс остановки происходит посылкой сигнала SIGTERM, если в течение 3 секунд отключение не произошло, посылается сигнал SIGKILL.

Разработка и отладка VPN-плагина


Сборка VPN-плагина «Континент-АП» и его компонентов производится на виртуальной машине (Virtual Box) ОС Sailfish Build Engine, входящей в состав Sailfish SDK. Для сборки плагина необходимы библиотеки: connman-devel, dbus-1, glibs-2.0, которые устанавливаем, залогинившись по ssh:

ssh -p 2222 -i ~/SailfishOS/vmshare/ssh/private_keys/engine/mersdk mersdk@localhost

Используем утилиту sb2 (Scratchbox 2) – toolkit для кросс-компиляции. Выполняем установку нужных пакетов для платформ i486 и armv7hl:

sb2 -t SailfishOS-3.0.1.11-i486 -m sdk-install -R zypper -n in cmake patchelf chrpath connman-devel systemd-compat-libs systemd-devel 

sb2 -t SailfishOS-3.0.1.11-armv7hl -m sdk-install -R zypper -n in cmake patchelf chrpath connman-devel systemd-compat-libs systemd-devel 

Systemd-compat-libs и systemd-devel нужны для вывода в системный журнал с помощью функции sd_journal_print. Cmake мы устанавливаем, так как наш проект его использует, это сильно упрощает сборку под разные платформы.

Запускаем сборку VPN-плагина и его компонентов через sb2 sdk-build:

sb2 -t SailfishOS-3.0.1.11-armv7hl -m sdk-build cmake . && make
sb2 -t SailfishOS-3.0.1.11-i486 -m sdk-build cmake . && make


Далее собранные бинарные файлы и библиотеки помещаем в наш UI проект, который содержит SPEC файл для генерации RPM-пакета дистрибутива «Континент-АП» Sailfish, немного скорректировав в SPEC файле секции инсталляции наших файлов в системные папки устройства, например:

%files
%defattr(-,root,root,-)
%{_sbindir}/continent
%{_libdir}/connman/plugins-vpn/continent-proto-plugin.so

Разработка плагина велась отдельно от UI на эмуляторе, идущем в составе Sailfish SDK, и в качестве утилит для отладки сильно помогала gdbus и journalctl с опцией -f в режиме постоянного вывода лога.

Например, создание экземпляра провайдера c помощью gdbus:

gdbus call --system --dest=net.connman.vpn --object-path /  --method net.connman.vpn.Manager.Create "{
'Type': <'continent'>, 
…	
}"

В качестве тестовых устройств выступали модели INOI R7(телефон), INOI T8(планшет) и эмулятор на базе VirtualBox

Полезные ссылки:


  1. Исходный код ConnMan, адаптированный под ОС Sailfish, можно найти здесь.
  2. Skeleton – проект плагина
  3. sailfishos.org/wiki




К сожалению, не доступен сервер mySQL