brainstate

derangedabbot/yandex-station-2-airplay · source

Snapshot 0 · 2026-11-04T12:47:28Z · Shell · MIT

Content fingerprint877373afa7db83ff9ccc8679c567f0fc6d0db62f055847084ce8ac1721864909

GitHub description at capture

Root на Яндекс.Станции 2 (платформа yandexmidi) и добавление нативного AirPlay 2: метод, закрепление доступа, приёмник shairport-sync, панель, подсветка, голос

README at capture

# Root + AirPlay на Яндекс.Станции 2 (платформа `yandexmidi`)

Практический отчёт: как получить root на «Яндекс Станции 2» через подмену облачного
конфига, закрепить доступ и **добавить AirPlay**, которого в этой колонке нет.

Проверено на реальном устройстве: модель `YNDX-00051` («Станция 2»), фактическая платформа
**`yandexmidi`**, прошивка `0.369.1.50.*` (сборка от 21.10.2026).

> **Про названия — важная путаница.** Это «Станция 2» (`YNDX-00051`, второе поколение), а не
> «Станция Миди»: Миди — отдельная модель `YNDX-00054` с маленьким дисплеем, у неё другой софт.
> Слово `midi` в наших текстах — это **внутреннее имя платформы** Яндекса (`yandexmidi`: папка
> прошивки, пресет активации, файл диагностики ALSA), а не название продукта. Сама колонка о себе
> в `quasar.cfg` пишет `deviceName: "Яндекс.Станция (второе поколение)"` — это и есть «Станция 2».
> Проверить на своей: `grep -i deviceName /system/vendor/quasar/quasar.cfg`.

> **English summary.** This repo documents rooting a Yandex Station 2 (`yandexmidi` platform)
> via a cloud-config interception (leaked CA + the tooling from
> [hu553in/yandex-station-root](https://github.com/hu553in/yandex-station-root)), and — the part
> that was missing everywhere — turning the speaker into a working **AirPlay receiver** using
> `shairport-sync` from Alpine, with automatic handover of the sound device between the stock
> media daemon and the AirPlay receiver.

---

## ✨ Что здесь есть

| | Что | Куда смотреть |
|---|---|---|
| 🔓 | root на Станции 2 через подмену облачного конфига — и почему на нашей прошивке это сработало | [`docs/01-root.md`](docs/01-root.md) |
| 🔁 | закрепление доступа: `adbd` по Wi-Fi, постоянный SSH на `2222`, автозапуск | [`docs/02-persistence.md`](docs/02-persistence.md) |
| 📡 | **AirPlay 2**, которого в колонке не было: `shairport-sync`, мультирум, управление с колонки | [`docs/03-airplay.md`](docs/03-airplay.md) |
| 🎛 | обратное управление AirPlay: пауза, дальше, назад — с панели, голосом и из MPRIS | [`docs/03-airplay.md`](docs/03-airplay.md) |
| 🌐 | веб-панель на `socat`: статус, логи, громкость, «сказать Алисе», плеер «Звука» | [`docs/06-panel.md`](docs/06-panel.md) |
| 🟢 | подсветка кольца своим зелёным дыханием (кадровый буфер `/dev/fb0`) | [`docs/08-leds.md`](docs/08-leds.md) |
| 🎤 | свои голосовые команды без облака — через локальный канал Алисы | [`docs/09-voice.md`](docs/09-voice.md) |
| 🎵 | свой трек её же плеером: локальный раздатчик + `audio_play` | [`docs/11-playback.md`](docs/11-playback.md) |
| 💾 | USB-host: флешки, SD, Ethernet через Type-C (OTG) | [`docs/12-usb-host.md`](docs/12-usb-host.md) |
| 🧯 | восстановление после сброса и проверка, что всё на месте | [`docs/04-recovery.md`](docs/04-recovery.md), [`docs/10-verify.md`](docs/10-verify.md) |

**Готовый плеер «Звука» для колонки** живёт отдельно:
[zvuk-yandex-station-player](https://github.com/derangedabbot/zvuk-yandex-station-player) —
ставится одной командой `./scripts/install.sh <IP>`.

---

## 🔓 Откуда взялся способ

1. В октябре 2026 публично утёк приватный ключ CA (EM OV TLS CA – G3C, разбор —
   [Fluepke/emudrahahahahaha](https://github.com/Fluepke/emudrahahahahaha)). Колонки доверяют
   сертификатам этого CA, когда ходят за своим облачным конфигом.
2. Колонка при старте забирает конфиг с `quasar.yandex.net` и **слушается поля
   `system_config.adbd`** — то есть облако может включить ей сетевой ADB.
3. Инструменты для этого собраны в [hu553in/yandex-station-root](https://github.com/hu553in/yandex-station-root):
   DNS-подмена трёх имён, reverse-прокси на mitmproxy с сертификатом от утёкшего CA и подмена
   ответа `get_sync_info` (включает `adbd.remoteActivation` и `adbd.network`).

Наша работа — не изобрести это заново, а **проверить на Станции 2** и довести до состояния, в котором
колонкой можно пользоваться каждый день.

## 🧪 Что выяснилось на нашей прошивке

Автор метода пишет, что сборка `.50` отвергает цепочку сертификатов. **На `yandexmidi` это не так**:
подмена конфига на `0.369.1.50.*` проходит, `adbd` поднимается, `adb shell` отдаёт `uid=0(root)`.
Похоже, закрытие дыры разъехалось по платформам волной, и наша платформа в неё не попала (или попала позже).

Поэтому откат прошивки на старую сборку **не понадобился** — а именно этого мы ждали в начале.

## 🔌 Питание вилкой — часть нашего метода, а не случайность

> **Главное, что обычно не пишут: окно отладки мы ловили не «подождать и поймать», а выдёргиванием
> кабеля питания.** Сразу после сброса колонка уходит в обновление из `/bkupgrade` и может
> 10–15 минут показывать анимацию, не отвечая по сети. Мы ни разу этого не дожидались: питание
> выдёргивалось, колонка перезагружалась, и **на загрузке заново шла за конфигом** — вот в этот
> момент прокси и подменял ответ, а `adbd` поднимался на `5555`.

Как это выглядело на практике:

1. сброс → колонка «догорает» обновление;
2. **выдернуть питание** (обычная вилка, без выключения кнопкой);
3. включить обратно → загрузка → запрос конфига → окно ADB на 30–60 секунд.

Побочные следствия, которые надо знать:

* **Обновление ни разу не завершилось** — мы каждый раз обрывали его питанием. Поэтому «прошивка
  осталась `.50`» — факт, а «сброс перепрошивает колонку» — недоказанное утверждение.
* Резкие обрывы питания колонка переживала спокойно: поднималась с синим миганием, прошивка цела,
  `/data` на месте. Но это лотерея, а не безопасная процедура.
* Если дать обновлению доиграть и перезагрузиться штатно, окно закрывается — и подмена конфига
  уже не даёт эффекта. Похоже, именно поэтому у разных людей на одной и той же `.50` получается
  разный результат.

Дальше в [`docs/01-root.md`](docs/01-root.md) шаг с питанием описан в общем виде: там же сказано,
что ждать обновление не нужно, но и обрывать его — решение, за которое вы отвечаете сами.

## 🧰 Что понадобилось сверх авторского набора

Штатные `make status` / `make install` на Станции 2 **падают**: они рассчитаны на раскладку Mini 2
(отдельный `/system`, другой скрипт активации). Всё, что ниже, сделано руками:

| Отличие Станции 2 | Следствие |
|---|---|
| Корень `ubi0:rootfs / ubifs ro`, но **перемонтируется в rw** (`mount -o remount,rw /`) | можно писать в `/etc` и `/system` |
| Отдельного маунта `/system` нет | `activate_adb.sh` живёт в `/system/vendor/quasar`, но это каталог на корне |
| `/data` → симлинк на `/bkupgrade/data` (ubi1_0, rw) | метки и утилиты переживают перезагрузку |
| Активация ADB — через `onboot.sh` → `activate_adb.sh check` | достаточно создать метки в `/data/quasar` |
| Прикладной слой Яндекса — **32-бит ARM**, система 64-бит | сторонние бинарники — только aarch64-musl |
| Busybox `tar` без `-z` | распаковка: `gzip -dc f.tar.gz \| tar -xf -` |
| Облако при своём конфиге **удаляет метки и гасит adbd** | ADB — короткое окно, постоянный канал — SSH |

Подробный разбор: [`docs/01-root.md`](docs/01-root.md), закрепление — [`docs/02-persistence.md`](docs/02-persistence.md).

## 📡 AirPlay — то, чего в колонке не было

Приёмник — **`shairport-sync 5.5.1` с AirPlay 2**, собранный под aarch64/musl в контейнере
(статический мини-ffmpeg + `nqptp`), плюс `avahi` для объявления в сети. Колонка появляется на
iPhone/Mac как **«Яндекс Станция»** и играет в свои динамики. Сборка — `docs/07-airplay2-build.md`.

Пришлось решить две вещи, на которых всё обычно и останавливается:

1. **mDNS.** У колонки уже есть свой mDNS — демон `maind` держит UDP 5353 и публикует
   `yandex-station-gen2-<id>.local`. Avahi с другим именем получает конфликт PTR-записей, отзывает
   адреса и переименовывается по кругу. Решение: дать avahi **точно то же имя хоста**, что у колонки.
2. **Эксклюзивный звук.** `maind --mediad` держит `pcmC0D0p` и не пускает никого (`EBUSY`).
   Решение — распорядитель: при подключении к AirPlay он останавливает медиа-демон, а после сессии
   перезапускает приёмник (тот отпускает устройство, но остаётся в сети) и поднимает демон обратно.
   В итоге Алиса жива, AirPlay доступен всегда, а звук переходит по очереди.

**Управление с колонки.** AirPlay 2 не даёт обратного управления через DACP, но механизм есть:
plist `sendMediaRemoteCommand` на канал событий аудио-сессии. Мы его включили — пауза, «дальше»,
«назад» работают и с Mac, и с iPhone, причём командовать можно с самой колонки: кнопками панели,
голосом или `airplay-ctl.sh`. Детали и грабли: [`docs/03-airplay.md`](docs/03-airplay.md),
шаг 6.

Рецепт целиком: [`docs/03-airplay.md`](docs/03-airplay.md).

**Чем это отличается от рецепта для Станции Мини 3** ([hackalice.me](https://hackalice.me/recipes/airplay/)).
Там берут готовый пакет `shairport-sync 4.3.7`, патчат в нём 4 байта `airplayd` и вешают хуки на
паузу. Отсюда четыре отличия, которые стоит знать до выбора пути:

* **патча нет** — приёмник у нас свой, поэтому после переустановки пакета AirPlay ничего не
  отваливается (у них патч и хуки исчезают, скрипт надо запускать заново), и время на дисплее не
  при чём (у них это побочный эффект патча, лечится флагом `--no-patch`);
* **управление с колонки** — пауза, «дальше», «назад» уходят источнику через
  `sendMediaRemoteCommand`; у них обратного управления нет, «Play надёжнее нажимать с телефона»;
* **владелец звука определяется по TCP-сессии**, а не по событиям старт/пауза: у них на стыке
  кнопку могут поймать оба демона, у нас распорядитель отдаёт устройство и возвращает Алису сам;
* **mDNS и ALSA решены явно** — avahi получает имя хоста колонки (иначе устройство «то появляется,
  то нет»), приёмник берёт свой `ALSA_CONFIG_PATH`, потому что системный тянет 32-битные плагины.

В минус нам: у них путь короче — сборки нет вовсе. Подробный разбор по пунктам, включая «что
общего» и почему модели сравнивать напрямую нельзя: [`docs/13-vs-airplay-recipe.md`](docs/13-vs-airplay-recipe.md).

## 📂 Что лежит в репозитории

```
docs/
  01-root.md          — получение root на Станции 2: пошагово, с отличиями от Mini 2
  02-persistence.md   — SSH-автозапуск, заморозка обновлений, сторож
  03-airplay.md       — AirPlay: mDNS, ALSA, распорядитель звука
  04-recovery.md      — как откатить всё назад
  06-panel.md         — веб-панель: статус, кнопки, логи
  07-airplay2-build.md — сборка AirPlay 2: статический ffmpeg, shairport-sync, nqptp
  08-leds.md         — подсветка кольца: framebuffer, паттерны, привязка к AirPlay
  09-voice.md        — голос: текст распознавания из локального IPC Алисы (127.0.0.1:9875)
  10-verify.md       — проверка «всё ли живо» одной командой (15 пунктов, ничего не меняет)
  13-vs-airplay-recipe.md — чем наш AirPlay отличается от рецепта для Станции Мини 3
scripts/
  device/             — скрипты, которые живут на колонке
  mac/verify-station.sh — проверка состояния колонки по SSH: 15 пунктов, только чтение
  build/airplay2/     — сборка AirPlay 2 (nqptp, shairport-sync, мини-ffmpeg) в контейнере
  build/led-patterns.py — генератор кадров подсветки
  build/              — сборка musl-набора утилит из пакетов Alpine
  mac/                — то, что запускается с компьютера
```

Инструменты (собранные из подписанных пакетов Alpine: `jq`, `strace`, `lsof`, `tmux`, `rsync`,
`dig`, `shairport-sync`, `mpg123`, `alsa-utils`, `dropbear`, `avahi`, `dbus`) в репозиторий не
включены — соберите сами, см. [`docs/03-airplay.md`](docs/03-airplay.md) и
[`docs/05-tools.md`](docs/05-tools.md).

## 💾 USB-host: флешки, SD-карты, Ethernet

Порт Type-C на Станции 2 умеет быть хостом — производитель просто не коммутирует его: по
умолчанию он отдан gadget-контроллеру, а xHCI при загрузке умирает (`xHCI host not responding`).
Переключение — четыре регистра USB2-PHY, всё в рантайме. Проверено: хаб, флешка FAT32, SD-карта
в картридере, USB-Ethernet (`eth0`). Есть защита от бутлупа и откат без перезагрузки.

Рецепт у нас: [`docs/12-usb-host.md`](docs/12-usb-host.md), скрипты —
[`scripts/device/otg/`](scripts/device/otg/). Основа — рецепт для Станции Лайт с
[hackalice.me](https://hackalice.me/recipes/usb-host/) ([Marker284/yandex-station-extended](https://github.com/Marker284/yandex-station-extended));
наша часть — проверка на Midi, автозапуск и грабли.

## 🌐 Веб-панель

Чтобы не лезть в SSH из-за каждой мелочи — страница на `socat` (в busybox этой прошивки нет
`httpd`), без единого нового пакета: статус приёмника и демона, кнопки «перезапустить AirPlay»,
«поднять Алису», «перезагрузить», хвосты логов, громкость ползунком, карточка «Сказать Алисе»
и управление плеером Звука: поиск с «+» (трек / альбом / исполнитель), очередь с переходом,
удалением и кнопками «Альбом»/«Исполнитель», «Очистить очередь».

Слушает только домашнюю подсеть (`socat …,range=`), токена нет — в своей сети он только мешает.
Логи показываются по Москве, хотя на диске время UTC.

```
http://<STATION_IP>:8081
```

Подробно: [`docs/06-panel.md`](docs/06-panel.md).

## 🟢 Подсветка: кольцо дышит, когда играет AirPlay

Кольцо станции висит на `/dev/fb0`: **89 пикселей в строку по 4 байта (R, G, B, X)**. Штатные
анимации в `/system/vendor/quasar/ledpatterns/panel/*.led` — ровно те же кадры по 356 байт.
То есть светом можно управлять напрямую, без облака и директив.

Сделано: пока идёт AirPlay — плавное «дыхание» триколором (белое → синее → красное, смена цвета
в полной темноте), после остановки — гаснет. Живёт в распорядителе звука.

Подробности, включая грабли (busybox-awk портит байты >127, AirPlay 2 держит TCP-соединение
после остановки и ломает детект): [`docs/08-leds.md`](docs/08-leds.md).

## 🎤 Голос: свои команды без облака

Станция отдаёт **готовый текст распознавания** в локальный сокет `127.0.0.1:9875`
(4 байта длины + protobuf, поле `252` → статус `3` и текст). Свой слушатель ловит фразы
из списка и запускает что угодно — модели и микрофон трогать не нужно.

Сделано: слушатель живёт на колонке, автозапуск, дедупликация срабатываний, пять рабочих фраз —
«включи звук», «пауза», «продолжи», «дальше», «стоп». Главный вывод: её облако отвечает позже
локального слушателя, поэтому `next`/`resume` ставятся с задержкой 1.5 с — иначе играет её поток.
Подробности и грабли: [`docs/09-voice.md`](docs/09-voice.md).

## 🎵 Свой трек её плеером

Самое полезное, что мы нашли: файл можно отдать **штатному плееру станции** командой
`audio_play` (protobuf через `externalCommandBypass`) — играет `maind --mediad`, Алиса жива,
громкость и пауза штатные. Обязательное условие — `Range` на нашем раздатчике, пауза ставится
только вместе с потоком (`set_pause: true`), а продолжение — повторным `audio_play` с `offset_ms`.
Схема целиком: [`docs/11-playback.md`](docs/11-playback.md).

## ⚠️ Риски, честно

- Сетевой ADB (`5555`) работает **без ключевой аутентификации**. Не держите его открытым в
  недоверенной сети; в нашей схеме основной канал — SSH по ключу.
- Прошивка заморожена: обновления отключены. Это осознанная плата — в новых сборках дыру закроют.
- Полный сброс колонки возвращает заводскую прошивку и **стирает `/data`** вместе с метками,
  утилитами и ключами SSH.
- Надёжность AirPlay в этой схеме — «работает и восстанавливается само», но это надстройка:
  после каждого отключения приёмник на несколько секунд исчезает из списка.
- Всё делается на свой страх и риск. Автор не связан с Яндексом.

## 📄 Дисклеймер

Материал публикуется в исследовательских целях: показать, как устроена активация и облачный
конфиг устройства, и что можно добавить к нему после получения доступа. Не используйте это на
чужих устройствах.

## 🔗 Связанные проекты

- **[zvuk-yandex-station-player](https://github.com/derangedabbot/zvuk-yandex-station-player)** — плеер «Звука»
  для этой же станции: Rust, один статический бинарник. Играет «Мой поток», управляется из
  панели и голосом. Собран поверх этой инфраструктуры (root, набор musl-утилит, панель,
  распорядитель звука).