Запуск мессенджера MAX в контейнере


А все так хорошо начиналось. Но, увы, есть у некоторых коллег «традиции» и они, кажется, не позволяют проводить полноценное тестирование и включать здравый смысл. И вроде бы, команды объективно высокопрофессиональных программистов и тестировщиков, devops. И…вот так… Ну почему? Почему так-то?
Итого, дело было не в паранойе и не в абстрактной безопасности. Дело было в обновлении самого мессенджера.
Под Linux данное ПО распространяется путем добавления репозиториев и установки оттуда. При этом репозитории едины для всех DEB based. В то же время, в пакете отсутствует ряд проверок, а куда его вообще устанавливают и есть шансы запуститься. То есть, банально, при штатном apt upgrade в какой-то момент прилетает и устанавливается версия, что требует для свого запуска glibc-2.28 и никак не ниже. Да уж. Национальный мессенджер, повышенная ответственность…где? Ну почему?.. Это же перелицованный Там-Там, то есть, вот это всё в разработке уже более десяти лет. Почему такие детские болезни?
Ладно, может когда-нибудь все же повзрослеют. У нас же актуальная задача запустить новую версию без полной замены системы, а не страдать размышлениями.

Продолжая устоявшиеся традиции, я тоже не могу сделать все по-человечески. Обязательным условием должен быть эдакий странный экзерсиз.
В современном Linux за безопасное хранение паролей, токенов авторизации и секретов отвечает системная служба gnome-keyring (или KWallet в KDE). Приложения общаются с этой службой через специальную библиотеку libsecret. Но внутри изолированного Docker-контейнера нет запущенной графической оболочки GNOME и нет кошелька паролей gnome-keyring. Мессенджер обращается к системной библиотеке libsecret, та пытается найти кошелек, получает жесткий отказ, и в коде мессенджера происходит критический сбой.
Для функционирования реального кошелька паролей требуется запущенный системный демон шифрования, привязанный к конкретной пользовательской сессии хоста через шину данных D-Bus. Проброс этой шины внутрь контейнера осуществить очень просто, но ведь из любви к искусству нужно сделать всё через ухо. И можно отговориться, что проброс d-bus сильно снижает изоляцию и защищенность. Хотя эта отговорка хоть как-то актуальна, конечно, если забыть, что изоляций как раз не цель, а вынужденное средство.
Итак, сделаем заглушку-эмулятор, чтобы мессенджеру только казалось, что он успешно взаимодействует с keyring, на самом деле ничего никуда не писалось и авторизация была актуальной только в оперативной памяти в рамках активной сессии приложения.
Создадим файл следующего содержимого:
# cat libsecret_mock.c

#include
#include

void* secret_password_store_sync(void* a, void* b, void* c, void* d, void* e, void* f, void* g) { return NULL; }
void* secret_password_lookup_sync(void* a, void* b, void* c, void* d) { return NULL; }
bool secret_password_clear_sync(void* a, void* b, void* c) { return true; }
void* secret_service_get_sync(int flags, void* cancellable, void* error) { return NULL; }
void* secret_password_storev_sync(void* a, void* b, void* c, void* d, void* e, void* f, void* g) { return NULL; }
void* secret_password_lookupv_sync(void* a, void* b, void* c, void* d) { return NULL; }
bool secret_password_clearv_sync(void* a, void* b, void* c) { return true; }
void* secret_password_searchv_sync(void* a, void* b, void* c, int flags, void* cancellable, void* error) { return NULL; }
void secret_password_free(void* password) {}
void secret_value_unref(void* value) {}
bool secret_item_delete_sync(void* item, void* cancellable, void* error) { return true; }
void* secret_item_get_secret(void* item) { return NULL; }
void* secret_item_get_attributes(void* item) { return NULL; }
void* secret_item_get_label(void* item) { return NULL; }
void* secret_service_search_sync(void* a, void* b, void* c, int flags, void* cancellable, void* error) { return NULL; }
bool secret_service_storev_sync(void* a, void* b, void* c, void* d, void* e, void* f, void* g, void* h) { return true; }
void* secret_schema_new(void* name, int flags, ...) { return NULL; }
bool secret_collection_delete_sync(void* collection, void* cancellable, void* error) { return true; }
void* secret_retrievable_get_attributes(void* r) { return NULL; }
void* secret_retrievable_get_label(void* r) { return NULL; }
unsigned long long secret_retrievable_get_created(void* r) { return 0; }
unsigned long long secret_retrievable_get_modified(void* r) { return 0; }
void secret_retrievable_retrieve_secret(void* r, void* c, void* callback, void* user_data) {}
void* secret_retrievable_retrieve_secret_sync(void* r, void* c, void* error) { return NULL; }
void secret_password_store() {}
void secret_password_lookup() {}
void secret_password_clear() {}
void secret_service_get() {}
void secret_item_delete() {}
void secret_password_storev() {}
void secret_password_lookupv() {}
void secret_password_clearv() {}
void secret_password_searchv() {}

Теперь создадим Dockerfile
#cat Dockerfile

FROM ubuntu:26.04

# 1. Установка базовых инструментов, компилятора и настройка локали UTF-8
RUN apt-get update && apt-get install -y locales gcc curl gpg fontconfig fonts-dejavu \
&& locale-gen ru_RU.UTF-8 \
&& update-locale LANG=ru_RU.UTF-8 LC_ALL=ru_RU.UTF-8
ENV LANG=ru_RU.UTF-8
ENV LC_ALL=ru_RU.UTF-8

# 2. Подключение официального репозитория мессенджера MAX
RUN mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://download.max.ru/linux/deb/public.asc | gpg --dearmor -o /etc/apt/keyrings/max.gpg \
&& echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/max.gpg] https://download.max.ru/linux/deb stable main" | tee /etc/apt/sources.list.d/max.list

# 3. Установка мессенджера и графического/аудио стека
# Да, в том числе libxaw7/libxmuu1, на полном серьезе. Они используются приложением. Это интересно сочетается с заглавной проблемой libc.
RUN apt-get update && apt-get install -y max \
libglx0 libgl1 libgbm1 libegl1 \
libxrender1 libxtst6 libxi6 libxcomposite1 libxdamage1 libxrandr2 libxcursor1 libxkbfile1 \
libnspr4 libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
libxkbcommon0 libxkbcommon-x11-0 \
libxcb-cursor0 libxcb-util1 libxcb-shape0 libxcb-res0 \
libasound2t64 libxv1 libice6 libsm6 libfontenc1 libxaw7 \
libxinerama1 libxmu6 libxpm4 libxt6 libxmuu1 libxss1 libxft2 \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

# 4. Компиляция и развертывание заглушки libsecret во все каталоги
COPY libsecret_mock.c /tmp/libsecret_mock.c
RUN gcc -shared -o /usr/lib/x86_64-linux-gnu/libsecret-1.so.0 /tmp/libsecret_mock.c -fPIC \
&& cp /usr/lib/x86_64-linux-gnu/libsecret-1.so.0 /usr/share/max/bin/max-service/lib64/libsecret-1.so.0 \
&& cp /usr/lib/x86_64-linux-gnu/libsecret-1.so.0 /usr/share/max/lib64/libsecret-1.so.0 \
&& rm -f /tmp/libsecret_mock.c

# 5. Интеграция шрифтов DejaVu для Qt6 и настройка групп
# Копирование шрифтов нужно из-за наблюдаемой странности max-service (дочернего процесса для звонков)
RUN mkdir -p /usr/share/max/lib64/fonts \
&& cp /usr/share/fonts/truetype/dejavu/DejaVuSans*.ttf /usr/share/max/lib64/fonts/ \
&& chmod 1777 /tmp \
&& usermod -aG audio ubuntu

# 6. Делаем /ubuntu жесткой ссылкой на /home/ubuntu. Всплыла некая частная особенность старта max-service (дочернего процесса для звонков).
RUN ln -s /home/ubuntu /ubuntu

# 7. Переключение на пользователя ubuntu с его родным HOME
USER ubuntu
WORKDIR /home/ubuntu

CMD ["max"]

Соберем образ для контейнера
docker build -t max-messenger:secure-prod .

Создадим на хосте папку для проброса в контейнер.
Здесь сразу создается подпапка .tmp, это тоже артефакт-особенность, устраняет предупреждение в логах Invalid cross-device link в процессе внутренней работы мессенджера.
mkdir -p $HOME/.max_messenger_profile/.tmp
И папку для обмена документами с мессенджером в контейнере. Тут вкусовщина, как удобнее. Я за отдельную папку, но можно хоть весь свой профиль с хоста.
mkdir -p $HOME/max_downloads

Теперь осталось только запустить и залогиниться. Напоминаю: логиниться каждый раз, хотя профиль легко сохраняется, токен доступа скидывается.
Пробросим с хоста аудио-видео, папки для профиля и для сохранения/обмена документами. Не стоит также забывать про таймзону.

docker run -it --rm \
--name max-messenger-stable \
-e DISPLAY=$DISPLAY \
-e QT_QPA_PLATFORM=xcb \
-e TMPDIR=/home/ubuntu/.local/share/ONEME/.tmp \
-v /tmp/.X11-unix:/tmp/.X11-unix:ro \
-v /run/user/$(id -u)/pulse/native:/tmp/pulse-socket \
-e PULSE_SERVER=unix:/tmp/pulse-socket \
-v $HOME/.max_messenger_profile:/ubuntu/.local/share/ONEME \
-v $HOME/max_downloads:/ubuntu/Downloads \
--ipc=host \
--device /dev/dri \
--device /dev/video0 \
-e ELECTRON_DISABLE_SANDBOX=1 \
-v /etc/localtime:/etc/localtime:ro \
-v /etc/timezone:/etc/timezone:ro \
max-messenger:secure-prod

На всякий случай, уточняю: ONEME это оригинальное и реальное имя папки, создаваемой и используемой самим мессенджером.


Ваш отзыв