Введение
Я работаю программистом в отделе разработки и тестирования средств защиты мобильных платформ компании «Код безопасности». Перед командой мобильной разработки была поставлена задача портировать кроссплатформенную библиотеку абонентского пункта «Континент-АП», которая уже успешно функционировала на IOS и Android. Основная проблема заключалась в том, что ОС Sailfish не так хорошо документирована, как Android или IOS, но спасибо ребятам из «Открытые Мобильные Платформы», которые поделились документацией.
Архитектура VPN-api в ОС Sailfish
За все сетевые подключения в ОС Sailfish отвечает служба ConnMan, а именно:
- сканирование Wi-Fi сетей и сетей сотовой связи и подключение к ним;
- расшаривание подключения (Wi-Fi hotspot);
- перевод, вывод соединений в режим полета;
- управление VPN-соединениями.
Про последний пункт я расскажу чуть подробнее. В составе ConnMan есть несколько типов предустановленных плагинов VPN. QML-виджеты для создания и настройки подключения интегрированы в прошивку.
Рис. 1 Системное меню ОС Sailfish для настройки и управления VPN-соединениями
UI нашего VPN-клиента представляет из себя RPM-пакет и при установке не встраивается в системное окно VPN в разделе Settings, а выглядит, как отдельное приложение. Про разработку UI, возможно, будет отдельная статья, поэтому следующее повествование будет про разработку ConnMan плагина на С/C++.
Рис. 2 Графический интерфейс «Континент-АП» Sailfish
VPN-api ОС Sailfish состоит из следующих компонентов, покажем на примере нашего VPN-клиента:
- ConnMan – процесс, запускаемый при старте ОС Sailfish.
- connman-vpnd – процесс-демон, запускаемый ConnMan и служащий для управления VPN-соединениями различных провайдеров, инициализацией и деинициализацией tun-интерфейса, назначением на него сетевых настроек (IP-адресов, маршрутов, DNS-серверов), полученных по DBus. В нашем случае провайдер с именем continent.
- VPN-плагин continent-proto-plugin.so – библиотека, имеющая макро-объявление для загрузки в runtime и функции, вызываемые при вызове методов интерфейса net.connman.vpn.Connection.
- Фоновое приложение (бинарный файл /usr/sbin/continent) – консольный клиент для подключения к СД (Серверу доступа «Континент»), получения от него сетевых настроек, которые он передает в connman-vpnd.
- ConnMan Task – процесс для запуска, остановки и контроля запущенного консольного клиента.
- DBus-api – представляет connman-vpnd, а именно net.connman.vpn с интерфейсами net.connman.vpn.Manager, net.connman.vpn.Connection.
Рис. 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
Полезные ссылки:
- Исходный код ConnMan, адаптированный под ОС Sailfish, можно найти здесь.
- Skeleton – проект плагина
- sailfishos.org/wiki