Как написать D-Bus сервис, работающий на системной шине, на Rust
Привет, Хабр! На связи Федотов Максим, ведущий разработчик в «Открытой Мобильной Платформе». В этой статье я хочу поделится нашим опытом создания D-Bus-сервиса на Rust, который работает на системной шине.
Если Вы только недавно начали изучать темы ОС GNU/Linux, Rust и D-Bus, но при этом Вам уже стало тесно в рамках простых базовых примеров и хочется зайти немного поглубже, то эта статья для Вас.
В ней мы рассмотрим, как написать D-Bus-сервис на Rust, который:
работает на системной шине,
даёт подключиться к себе только суперпользователю,
при этом стартует только при обращении к нему, не тратя ресурсы впустую.
Мы в «Открытой Мобильной Платформе» разрабатываем Аврора Центр — UEM систему по централизованному управлению пользовательскими устройствами в организации. В своё время, когда мы добавляли функционал по удалённому управлению рабочим столом, нам надо было адаптировать проект RustDesk под Альт и интегрироваться с ним, чтобы можно было через клиент Аврора Центр конфигурировать и инициировать сеанс удалённого подключения. Часть требований, представленных выше, как раз и были взяты из этого сценария. На тот момент я не смог найти готового примера, который бы сразу ответил на мои, может и не очень сложные, но всё же имеющиеся вопросы. Так что я подумал, что будет неплохо написать небольшую статью об этом вместе с примером и со всеми необходимыми конфигурационными файлами.
Наш пример будет представлять из себя заготовку проекта со всеми требуемыми конфигурационными и сервисными файлами, которую можно дальше дорабатывать под свои нужды.
Если Вам комфортнее не читать статью, а сразу смотреть исходный код, то добро пожаловать в пример: https://github.com/omprussia/rust-d-bus-service.
Смотрим на требования и варианты их удовлетворения
В моей изначальной ситуации нам требовалось интегрироваться с проектом, написанным на Rust и работающим под ОС семейства GNU/Linux. Это, собственно, и определило базовый стек используемых технологий. Для взаимодействия программ между собой был выбран D-Bus, как созданный как раз для общения между программами в рамках одного хоста в мире ОС семейства GNU/Linux.
Теперь давайте рассмотрим требования уже в разрезе D-Bus и старта сервиса:
Взаимодействие не должно зависеть от пользовательской сессии — поэтому будем запускать наш D-Bus-сервис на системной шине.
Требуется ограничить доступ к нашему сервису — доступ должен быть предоставлен только суперпользователю.
Нам хочется иметь стабильную работу с нашим функционалом и не тратить ресурсы впустую — это значит, что наш сервис должен стартовать, когда к нему обращаются, чтобы запуститься заново, если предыдущий запрос по какой-то причине закончился падением сервиса, и не работать, когда запросы отсутствуют.
Немного теории по D-Bus
D-Bus представляет собой средство межпроцессного взаимодействия (IPC — interprocess communication), т.е. позволяет обмениваться сообщениями между процессами в рамках одного устройства. Причём он обладает возможностью не просто обмениваться сообщениями между процессами, но ещё и настраивать параметры этого взаимодействия, например, уровни доступа.
В D-Bus есть разделение на системную и сессионную шину обмена сообщениями:
Системная шина существует в единственном экземпляре в системе и работает вне зависимости от пользовательской сессии. Как правило, предназначена для работы непосредственно самой ОС.
Сессионная шина существует уже не в единственном экземпляре. Она создаётся для каждой пользовательской сессии (соответственно, не существует без пользовательской сессии).
Также стоит упомянуть, что системная и сессионная шины функционируют в разных процессах и потоки данных обрабатываются раздельно.
Подробнее эти моменты можно прочитать в официальной документации D-Bus Tutorial.
Реализация
Я упоминал, что не смог найти пример, удовлетворяющий всем моим требованиям, но что же я находил? В основном, это были примеры проектов на GitHub, которые работали по сессионной шине и не имели интересующих меня настроек доступа. Ещё я нашёл один довольно интересный генератор, который прямо за Вас может накидать скелет приложения по работе с D-Bus на Rust https://github.com/diwic/dbus-rs/tree/master/dbus-codegen. Вещь интересная, рекомендую с ней познакомиться, но, к сожалению, она тоже не может сгенерировать сервис, который будет работать на системной шине, что не удовлетворяет нашим требованиям. Ещё стоит посмотреть на примеры, которые есть прямо в README проекта https://github.com/diwic/dbus-rs.
Давайте напишем простой D-Bus-сервис, который удовлетворяет приведённым выше требованиям, будет производить некую обработку пришедших данных и испускать сигнал по результатам успешной обработки вместе с обработанными данными.
Для того, чтобы работать с D-Bus в Rust, мы возьмём две библиотеки: dbus, которая даст нам возможность подключиться к системному (в нашем случае) D-Bus-серверу, и dbus_crossroads, которая зарегистрирует наши объекты, интерфейсы, методы и сигналы.
За основу возьмём пример этого D-Bus-сервиса на сессионной шине и переделаем его под свои нужды.
В примере всё начинается с создания подключения к сессионной шине Connection::new_session()
, но нам-то нужна системная шина, так что перепишем код под системную шину, заменив new_session
на new_system
. Но только этого будет недостаточно, поскольку для работы сервиса на сессисонной шине требуется только исполняемый файл самого сервиса — никаких конфигурационных файлов не требуется. А вот если сервис должен работать на системной шине, то тут уже потребуется конфигурационный файл — про него я расскажу чуть позже. Создаём соединение с системным D-Bus-сервером и запрашиваем имя нашего сервиса:
let connection: Connection = Connection::new_system()?;
connection.request_name(DBUS_NAME, false, true, false)?;
Регистрируем обработчики запросов к нашему сервису:
let mut cr = Crossroads::new();
let iface_token = cr.register(DBUS_NAME, handle_client_message);
cr.insert(DBUS_PATH, &[iface_token], ());
Тут основная идея примера, на котором мы базировались, сохраняется, но для удобства поддержания кода проекта разделим всё на отдельные функции. Т.е. мы свою логику расположим не прямо в лямбда-функции внутри register
, а вынесем отдельно в обработчик запросов handle_client_message
.
Стартуем наш сервис для обработки входящих запросов:
cr.serve(&connection)?;
В обработчике запросов зарегистрируем сигнал об успешной обработке пришедших данных и сам метод для обработки данных:
fn handle_client_message(builder: &mut IfaceBuilder<()>) {
builder.signal::<(String,), _>(DBUS_SIGNAL_DATA_PROCESSED.to_string(), ("ProcessedData",));
builder.method(
DBUS_METHOD_PROCESS_DATA,
("data",),
("ret",),
move |ctx: &mut Context, _, (data,): (String,)| {
match do_some_data_processing(data) {
Ok(processed_data) => {
send_processed_data(processed_data, ctx);
return Ok((DBUS_METHOD_RETURN_SUCCESS.to_string(),));
},
Err(err) => {
let error : String = "Error during data processing: ".to_string() + &err.to_string();
println!("{error}");
return Err((DBUS_METHOD_RETURN_FAIL,error).into());
}
}
},
);
}
Обработку данных оставим заглушкой — при желании там можно реализовать то, что потребуется в Вашем проекте:
fn do_some_data_processing(data: String) -> Result {
let processed_data : String = "processed: ".to_string() + &data;
return Ok(processed_data);
}
Получив запрос на обработку данных и обработав его, можно отправить сигнал с результатом обработки:
fn send_processed_data(processed_data: String, ctx: &mut Context) {
let msg = Message::signal(
&DBUS_PATH.into(),
&DBUS_INTERFACE.into(),
&DBUS_SIGNAL_DATA_PROCESSED.into());
ctx.push_msg(msg.append1(processed_data));
}
Это основное, что нам требуется написать на Rust. Теперь давайте создадим все необходимые конфигурационные файлы, чтобы это всё заработало, причём так, как нам требуется.
Для работы нашего сервиса на системной шине требуется конфигурационный файл для D-Bus, поскольку по умолчанию система не позволит нам работать на системной шине без него. Этот конфигурационный файл должен располагаться по пути /etc/dbus-1/system.d/org.example.rusty.conf
. Выглядеть в нашем случае он будет следующим образом:
В параметре user
прописываем суперпользователя как единственного, кто может взаимодействовать с нашим сервисом.
Подробнее про этот конфигурационный файл и его возможности можно прочитать в секции CONFIGURATION FILE в D-Bus Daemon.
Чтобы наш сервис стартовал при обращении по D-Bus к нему, необходимо создать сервисный файл и поместить его по пути /usr/share/dbus-1/system-services/org.example.rusty.service
. Сам файл выглядит следующим образом:
[D-BUS Service]
Name=org.example.rusty
Interface=org.example.rusty
Exec=/usr/bin/rusty_d_bus
User=root
Получается, что у нас появляются следующие конфигурационные файлы:
org.example.rusty.conf — конфигурационный файл, который требуется для того, чтобы наш D-Bus сервис мог работать на системной шине и только суперпользователь смог к нему подключиться.
org.example.rusty.service — сервисный файл для автозапуска D-Bus сервиса.
Обратите внимание на названия этих конфигурационных файлов — они так названы не просто так, а в соответствии с рекомендациями в D-Bus Daemon.
Дабы не заниматься ручным раскладыванием конфигурационных и исполняемых файлов, давайте всё это дело запакуем в установочный пакет. В рамках данного примера сделаем это для систем, основанных на RPM. Сборка установочного пакета будет производиться на основе spec-файла со следующим содержимым:
Name: rusty_d_bus
Version: 0.1.0
Release: 0
Summary: RPM package
Group: Applications/System
License: MIT
%description
Example D-Bus service written in Rust.
%prep
%build
%install
rm -rf %{buildroot}
mkdir -p %{buildroot}/usr/bin/
mkdir -p %{buildroot}/etc/dbus-1/system.d/
mkdir -p %{buildroot}/%{_datadir}/dbus-1/system-services/
install -m 744 $SOURCES_ROOT/target/release/rusty_d_bus %{buildroot}/usr/bin/rusty_d_bus
install $SOURCES_ROOT/d-bus/org.example.rusty.conf %{buildroot}/etc/dbus-1/system.d/
install $SOURCES_ROOT/d-bus/org.example.rusty.service %{buildroot}/%{_datadir}/dbus-1/system-services/
%files
/usr/bin/rusty_d_bus
%{_sysconfdir}/dbus-1/system.d/org.example.rusty.conf
%{_datadir}/dbus-1/system-services/org.example.rusty.service
%changelog
%post
systemctl daemon-reload || true
%preun
Тут мы не только файлы разложим, но ещё и systemctl daemon-reload
выполним, чтобы система узнала про наши конфигурационные файлы.
Подробнее про spec-файл можно почитать в RPM Packaging Guide.
Итоговая картина выглядит следующим образом:
У нас имеется сервисный файл, который позволит нашему сервису стартовать при обращении к нему по D-Bus.
При старте бинарного файла сервиса мы зарегистрируемся на системной шине — это нам доступно, поскольку у нас есть требуемый для этого конфигурационный файл.
И уже бинарный файл сервиса обработает запрос, вернув ответ.
Для более цельной картины рекомендую посмотреть проект, где всё описанное собрано воедино https://github.com/omprussia/rust-d-bus-service.
Ну вот собственно и всё. Теперь давайте соберём наш пакет, поставим его в систему и пообщаемся с нашим D-Bus сервисом.
Сборка и проверка
Сборка и пакетирование у нас помещаются в две простые команды:
cargo build --release
SOURCES_ROOT=`pwd` rpmbuild ./rpm/d-bus-service.spec -bb
Устанавливаем получившийся пакет и пробуем поговорить с нашим D-Bus-сервисом.
Давайте в одной вкладке эмулятора терминала запустим от суперпользователя отслеживание всех сообщений нашего D-Bus сервиса:
busctl monitor org.example.rusty
А в соседней пошлём команду от пользователя и увидим, что политики безопасности отрабатывают и от обычного пользователя наш запрос не будет обработан:
$ dbus-send --system --print-reply --dest=org.example.rusty /dbus/rust/service org.example.rusty.ProcessData string:'data_for_processing'
Error org.freedesktop.DBus.Error.AccessDenied: Rejected send message, 1 matched rules; type="method_call", sender=":1.1281" (uid=500 pid=15759 comm="dbus-send --system --print-reply --dest=org.exampl") interface="org.example.rusty" member="ProcessData" error name="(unset)" requested_reply="0" destination="org.example.rusty" (bus)
А вот если сделать то же самое от суперпользователя, то всё заработает:
# dbus-send --system --print-reply --dest=org.example.rusty /dbus/rust/service org.example.rusty.ProcessData string:'data_for_processing'
method return time=1723531268.050177 sender=:1.1718 -> destination=:1.1717 serial=3 reply_serial=2
string "org.example.rusty.ProcessData.Success"
и во вкладке с busctl monitor
мы увидим наш сигнал об обработке данных:
‣ Type=signal Endian=l Flags=1 Version=1 Cookie=4 Timestamp="Tue 2024-08-13 06:41:08.050885 UTC"
Sender=:1.1718 Path=/dbus/rust/service Interface=org.example.rusty Member=DataProcessed
UniqueName=:1.1718
MESSAGE "s" {
STRING "processed: data_for_processing";
};
Всё, заготовка пакета D-Bus сервиса на Rust готова.
Дальнейшее изучение темы
Чтобы не выискивать по всей статье полезные ссылки, собрал их все здесь в одном месте.
Что стоит изучить для углубления своих знаний:
D-Bus Tutorial
D-Bus Daemon
Crate dbus
Crate dbus_crossroads
dbus-rs
Пример D-Bus-сервиса, который был переделан
RPM Packaging Guide