Получение root-прав на CMF Phone 1 для запуска личной инфраструктуры дома.
Какое-то время моя личная инфраструктура жила на небольшом VPS у Hetzner. Там крутилось несколько веб-приложений, удалённый браузер под названием Surf, Caddy и привычный набор вспомогательных сервисов. Ничего особенно серьёзного. Всё работало — просто мне не нравилось за это платить.
Одно из приложений — Surf — сделало этот компромисс труднопереносимым. Самые дешёвые shared-машины были нормальными, пока Chrome не начинал делать реальную работу — в этот момент им не хватало ресурсов. Выделенные процессоры (dedicated CPU) это исправляют, но стоят достаточно дорого каждый месяц, чтобы личный браузер стал финансово сомнительным вложением.
Покупка ещё одной машины тоже не казалась заманчивым выходом. Цены на DRAM окончательно сошли с ума, так что собирать новый бокс с комфортным объёмом памяти было особенно не вовремя. Я посматривал на б/у мини-ПК и даже на мгновение подумывал превратить свой десктоп в сервер, когда им не пользуюсь. А потом вспомнил, что у меня уже есть CMF Phone 1.
Восемь ARM-ядер, 8 ГБ ОЗУ, 128 ГБ флэш-памяти, Wi-Fi 6, 5G-модем и встроенная батарея как ИБП — и всё это на SoC, которая, по моему мнению, слишком хороша, чтобы просто лежать в ящике. А я уже за неё заплатил. Смахнув пыль и немного поигравшись, я решил превратить телефон в сервер.
Сегодня он крутит Surf с управляемым экземпляром Chrome, мой личный финансовый трекер, сервис демонстрации экрана и горсть мелких веб-приложений. Они переживают перезагрузки, разворачиваются из Git и остаются доступными, когда телефон переключается между сетями. Так что именно эта машина теперь по-настоящему заменила VPS.
Самый чистый вариант казался таким: прошить обычный Linux-дистрибутив. У CMF Phone 1 есть порт postmarketOS, он загружается, а на странице устройства достаточно зелёных галочек, чтобы безрассудный человек поверил в успех. Этим безрассудным человеком оказался я.
Чему я уделил меньше внимания — так всему, что отмечено как сломанное: Wi-Fi, Bluetooth, аппаратное ускорение и большинство других вещей, которые делают телефон полезным как небольшой сервер. Я добрался только до заставки postmarketOS и чёрного экрана. В этот момент у меня не было ни сервера, ни телефона.
Восстановление стокового Nothing OS превратилось в отдельный квест. Утилита прошивки требовала Windows, поэтому я установил Windows в QEMU, бился с USB-passthrough и драйверами MediaTek, смотрел, как прошивальщик зависает, а потом перенёс процесс на настоящую установку Windows и восстановил заводские образы.
Он ожил, и урок был усвоен: Android уже имеет рабочие драйверы для каждой части этого железа. Wi-Fi, управление питанием, батарея, GPU, модем и все странные вендорские детали уже работают. Выбросить всё это ради более привычного userspace — ошибочный компромисс.
На самом деле мне не нужно было, чтобы телефон стал обычной Linux-машиной. Мне нужно было, чтобы он надёжно запускал Linux-приложения, пока Android продолжал делать то, в чём он хорош — работу с железом.
Вторая попытка оставила стоковый Android и сделала Termux основной средой.
Termux даёт мне OpenSSH, runit, Caddy, Cloudflared, пакетный менеджер и достаточно обычный Unix-инструментарий. Termux:Boot запускает супервизор и SSH после перезагрузки. Tailscale даёт телефону стабильный приватный адрес, так что с любой машины в моём tailnet я просто запускаю:
ssh cmf
Termux — не виртуальная машина. Его процессы выполняются поверх ядра Linux Android, но его Bionic-based userspace достаточно отличается от обычной установки Debian, так что существующие образы Linux-приложений нельзя просто так туда скопировать. Это разделение, впрочем, оказалось полезным: Termux остался небольшим control plane хоста, а каждое приложение приносило ту файловую систему Linux, которую оно ожидало.
Сервисы управляются через runit. Управление батареей Android отлично справляется со своей обычной работой и очень плохо — с устройством, притворяющимся сервером. Поэтому мой Ansible-билд также применяет Android-профиль хоста: устанавливает постоянный wake lock, отключает light и deep idle, исключает Termux, Termux:Boot и Tailscale из фоновых ограничений, отключает лимитер дочерних процессов, предотвращает засыпание Wi-Fi и настраивает Tailscale как always-on VPN.
Android boot
→ Tailscale always-on VPN
→ Termux:Boot
→ runit
→ resident services
→ local and public health checks
Это не обычный Linux-сервер. Здесь нет systemd, нет обычного Docker-демона, и нет смысла притворяться. Но это ядро Linux с очень способным userland поверх него — и этого, как оказалось, достаточно.
Большая часть моих приложений уже поставлялась как Linux ARM64 OCI-образы. proot-distro делал их удивительно простыми в запуске под Debian без изменения самих приложений.
PRoot перехватывает файловые и процессные операции в userspace и заставляет обычный процесс Termux верить, что он живёт внутри Debian root filesystem. Это не граница контейнера. Всё по-прежнему использует ядро Android, сетевое пространство имён и UID Termux. Но как слой совместимости приложений он крайне полезен, потому что не требует ни root, ни специального ядра.
Обычные веб-сервисы изначально работали таким образом. Каждое приложение получило проверенный root filesystem, loopback-порт и runit-сервис. Caddy работал напрямую в Termux и маршрутизировал имена хостов на эти порты.
Исключением стала чувствительная к производительности и задержкам нагрузка — браузер Surf. Запуск процессов, открытие библиотек, обход путей, чтение профилей Chrome и пересылка данных захвата — всё проходило через слой трансляции PRoot в userspace. Процессор был доступен, но Chrome не мог добраться до него эффективно. Тогда я получил root на телефоне — не чтобы заменить Android, а чтобы смонтировать ту же Debian-файловую систему должным образом и войти в неё через настоящий chroot.
Когда этот путь стал надёжным, оставлять мелких резидентов под PRoot потеряло смысл. Теперь они работают так же. Моя рабочая станция резолвит каждый ARM64-образ до точного дайджеста и экспортирует его файловую систему; Ansible проверяет и устанавливает её на телефон. Небольшой root-хелпер создаёт приватное mount-пространство, биндит нужные пути, входит в файловую систему через chroot, сбрасывает привилегии и запускает оригинальную entrypoint образа.
Ни Docker, ни компилятор не нужны на телефоне. Это по-прежнему среды совместимости, а не границы безопасности: резиденты делят ядро и сетевой стек Android, а приватные mount-namespace нужны в основном для предсказуемости монтирования и очистки.
Также я потратил слишком много времени, пытаясь соединить графический стек Debian с Mali GPU телефона через VirGL и Android Vulkan. Я получил галочки аппаратного композитинга рядом с битыми страницами и худшей производительностью… Скучный софтверный рендеринг оказался быстрее.
К этому моменту телефон мог запустить всё, но мне не нужен был «pet-сервер», собранный из команд, которые я забуду через неделю. Я перевёл весь хост в управляемое Ansible-состояние: версии, определения сервисов, маршруты, настройки питания, секреты и health-check'и — всё живёт в одном приватном репозитории.
Поток развёртывания примерно такой:
release or OCI image
→ checksum/digest pinned in Git
→ Ansible over SSH
→ versioned files on the phone
→ atomic current symlink
→ runit service
→ local health check
→ public edge check
Релизы фиксируются по дайджесту или контрольной сумме и устанавливаются в версионированные директории за атомарным симлинком current. Проваленная контрольная сумма или health-check останавливает развёртывание; откат означает возврат пина и повторное применение. Данные приложений живут отдельно от релизов.
После небольшого ручного бутстрапа — установить три Android-приложения, получить root, дать Termux доступ суперпользователя и авторизовать SSH — тот же репозиторий владеет всем остальным. С рабочей станции привести хост к заданному состоянию намеренно просто:
make phone
make phone-status
make phone-edge-check
Повторное применение не заменяет неизменённые runtime-файлы и не перезапускает здоровых резидентов. Что важнее — конфигурация полезна, если телефон умрёт: другой рутируемый ARM64-телефон можно привести к тому же состоянию без реконструкции истории шелла.
Следующей проблемой был ingress. Мой домашний интернет не идёт со статикой, как у VPS, и я не хочу открывать SSH или кучу случайных портов приложений через роутер. Также я хотел, чтобы телефон оставался телефоном в одном важном смысле: я могу отключить его, унести куда-то, подключить к интернету — и сервер по-прежнему со мной.
HTTP-приложения используют Cloudflare Tunnel. Cloudflared делает одно исходящее соединение с телефона, Cloudflare направляет каждый hostname через него, а Caddy маршрутизирует запрос на нужный loopback-сервис.
Internet
→ Cloudflare Tunnel
→ Caddy on 127.0.0.1
→ application on 127.0.0.1
Никакого inbound-правила на роутере для этих сервисов нет. Cloudflared нужен только исходящий коннект — если я перенесу телефон в другую сеть, туннель переподключится, и имена хостов последуют за ним. Tailscale делает то же самое для администрирования. Батарея телефона может перекрыть переезд, а публичные сервисы не заботит, какой Wi-Fi под ними.
Это был момент, когда телефонная связка стала слегка нелепой — в лучшем смысле. Я оставил его включённым дома, ушёл в офис, подключился по SSH с MacBook через Tailscale к рабочей станции и телефону и использовал оригинальный iPad через Surf, хостящийся на телефоне. VPS больше не было — и это ощущалось освобождающе.
У машины два слоя. Android и Termux владеют железом, сетью, ingress'ом и супервизией. Linux-резиденты получают ожидаемую файловую систему и в остальном не мешают хосту.
| Слой | Ответственность | Компоненты |
|---|---|---|
| Android / Termux (хост) | железо, сеть, ingress, супервизия | runit, Tailscale, Caddy, Cloudflared, DDNS, dashboard операций |
| рутированные Linux-резиденты | совместимость приложений и рабочие нагрузки | Surf и Chrome, Финансы, Screen Share, другие приложения |
Самая требовательная нагрузка — Surf, который приносит современный Chromium-браузер на старые iPhone и iPad. Телефон крутит десктопный Chrome (свежий arm64-релиз) и бэкенд Surf внутри Debian-runtime; iPad mini получает H.264-видео и аудио, отправляя обратно тач, клавиатуру, табы и навигационные команды. Именно Surf — причина, почему я так заботился о накладных расходах сисколов и задержках.
Рядом телефон хостит мой личный финансовый трекер. Он записывает регулярные доходы и расходы, разовые транзакции, месячные лимиты трат и прогнозирует баланс по дням вперёд. В отличие от заменяемых артефактов приложений, его SQLite-база содержит состояние, которое мне дорого, поэтому есть автоматические бэкапы вне устройства и протестированный путь восстановления.
Ни одно из этих имён не встроено в какой-то грандиозный «фреймворк телефонного сервера». Новый резидент — это ещё один иммутабельный артефакт, определение runit, явный контракт данных, health-check и опционально маршрут Caddy.
Последнее добавление — наблюдаемость (observability). Я по-прежнему могу зайти по SSH, проверить runit, полистать логи, проверить публичные маршруты и опросить Android напрямую, но больше не нужно делать это только чтобы понять, что происходит с машиной: один нативный сервис собирает загрузку CPU всех восьми ядер, память, хранилище, аптайм, батарею, температуру, локальную и публичную доступность и каждого обнаруженного runit-резидента. Он хранит ограниченную историю и отдаёт встроенный Vue-интерфейс по адресу https://dash.cmf, доступный только в LAN или tailnet.
Просмотр логов обнаруживает те же директории сервисов в runtime, так что добавление нового резидента не требует обучать UI его имени. Это гораздо приятнее, чем заходить по SSH только чтобы вспомнить, кто шумел.
Если у вас уже есть достаточно современный, рутируемый ARM64-телефон, лежащий без дела — это гораздо менее нелепо, чем звучит. Вы получаете тихое железо, низкое энергопотребление, флэш-хранилище, Wi-Fi, встроенный дисплей для восстановления и батарею, которая работает как крошечный ИБП. Стоковый Android уже поддерживает железо, а Termux и рутированный chroot достаточно, чтобы запустить удивительное количество обычного Linux-софта.
Я бы не стал хранить незаменимые данные на таком устройстве без автоматических бэкапов вне телефона и не рассматривал бы chroot'ы как изоляцию враждебных нагрузок. Рутинг расширяет границу доверия, Android остаётся необычным серверным хостом, а софтверный рендеринг десктопного Chrome не обгонит выделенную рабочую станцию с GPU.
Но для набора личных сервисов — особенно когда альтернативой является бесконечная оплата VPS, который либо медленный, либо раздражающе дорогой — это действительно полезный вариант, а не просто трюк.
Эта статья обсуждалась на Hacker News. В истинном духе HN удивительно большая часть обсуждения свелась к спору о заголовке. Мне понравился случайно случившийся лингвистический тред. :)