Внеполосный канал Windows ↔ Linux через звуковые разъёмы. Без IP, без PPP, без «модема поверх звука».
Сервер стоит в двух метрах от меня. На нём вроде бы всё есть: Wi‑Fi, VPN, sshd. А достучаться с ноутбука я не могу. Ethernet без линка, дефолтный маршрут уехал в туннель, DNS пустой, nftables режет то, что осталось.
Нужен канал, который не является сетью. Не тоннель через сломанный стек, а отдельный провод, которого брандмауэр не видит.
Я сделал консоль по двум аудиокабелям: с ноутбука уходят нажатия, обратно приходит текстовая сетка терминала 24×80 — то, что оператор видел бы на мониторе, а не поток stdout и не скриншот. Пакет xconn_channel 1.0.0, только стандартная библиотека Python. Первая сборка принята на живом железе: Ubuntu 26.04, Realtek ALC897. После одной установки с флешки сервер больше не трогали руками.
ноутбук ──наушники──► line-in сервер
◄──микрофон── аудиовыход
Два провода крест‑накрест. Отсюда имя: x_connector.
Репозиторий: github.com/DmNep/x_connector
Иллюстрация обмена по кабелю: сигнал и сетка терминала сервера.
1. Задача, которой нет у SSH
Типичный рассказ про «данные по звуку» начинается с ностальгии: кассеты, ZX Spectrum, акустический модем. У нас другая постановка.
Сеть на сервере нужна. Её надо починить. Ноутбук при этом нельзя воткнуть в тот же L2/L3: кривые сокеты, VPN, изоляция Wi‑Fi, «интернет есть, а до машины рядом ‑нет». Обычный SSH в этот момент бесполезен дважды. Во‑первых, до sshd ещё надо дойти по IP. Во‑вторых, даже если бы дошли, канал диагностики не должен зависеть от того самого стека, который сейчас разбираешь.
Отсюда запрет, который я сразу принял за константу и больше к нему не возвращался: не поднимать IP поверх звука. Ни PPP, ни SLIP, ни «софт‑модем, а дальше как обычно». Это вернуло бы маршрутизацию, DNS, nftables и VPN в середину аварийного канала. Канал умер бы в тот же день, когда он впервые понадобится.
Рабочий цикл не потоковый. Набрали команду — подождали около секунды ‑прочитали экран. Целевая скорость: «команда в секунду», не мегабиты. Практический набор, под который всё проектировалось:
-
ip a,ip r,ip neigh,ss -tunap -
resolvectl,journalctl -b,nft list ruleset -
правка netplan / systemd‑networkd / конфига VPN
-
перезапуск службы
-
небольшой файл в обе стороны (конфиг, скрипт, ключ)
Потоковое видео, большие архивы и GUI в задачу не входят. После починки сети канал не выбрасывается: это аварийная консоль на следующий раз. Значит, минимум подвижных частей, восстановление перетыканием кабеля, без долгого установочного рукопожатия.
2. Почему не модем, не PPP и не «клавиатура USB»
Вариант A — классический SSH поверх аудиоканала. Звук как последовательный порт, сверху ssh -o ProxyCommand=…. Получаешь шифрование, сессии, SFTP. Платишь трафиком, рукопожатием и зависимостью от живого sshd. На узком шумном канале установочный обмен SSH — отдельный квест: одно потерянное окно, и клиент думает, что сервера нет.
Вариант B — эмуляция клавиатуры. Ноутбук шлёт символы, сервер принимает их как ввод. Минимум данных, нет сетевой обвязки. Цена: нет шифрования, нужна сторона, которая легально кладёт ввод в tty, и нет передачи файлов «из коробки».
Я взял Вариант B как базу и надстроил то, чего в «голой клавиатуре» нет.
Прямое направление — не HID и не uinput «как будто клавиатура». После установки агент держит PTY с bash и пишет туда принятые символы. Это обычный вход в оболочку: Tab, Ctrl‑C, стрелки, длинная команда. Обратное направление по условию задачи — не лог и не растр, а картинка терминала в символах. Эмулятор VT100 на сервере снимает сетку строк × столбцов с курсором. Клиент рисует то же, что увидел бы человек на мониторе.
HID‑микроконтроллер (ATmega, RP2040: сервер видит клавиатуру ещё на getty, до любого агента) в первую сборку не вошёл. На установке у сервера были монитор и клавиатура. «Слепой подъём» — следующая фаза. USB в 1.0.0 закрывает одну задачу: перенести агент на флешке. Рабочий обмен по USB не идёт.
Шифрования нет сознательно: оператор физически стоит у машины. По кабелю поедут открытые конфиги и ключи — это принятый остаточный риск, не недосмотр. Отдельный путь утечки, от кабеля независимый: снимки экрана уходят в облако вместе с промптом, если консолью пользуется облачный ИИ. Если когда‑нибудь понадобится работа без физического присутствия, тонкий слой на libsodium дешевле, чем перестройка протокола. В открытый канал его потом не добавить «незаметно».
3. Физика тракта
Схема из двух однонаправленных проводов. На бумаге это полный дуплекс. На на деле — нет.
|
Ноутбук |
Сервер |
Коммутация |
|---|---|---|
|
Разнесённые разъёмы |
Разнесённые |
X двумя проводами |
|
TRRS |
Разнесённые |
Y со стороны ноутбука |
|
Разнесённые |
TRRS |
Y со стороны сервера |
|
TRRS |
TRRS |
Y с обеих сторон |
X‑схема: два однонаправленных провода крест‑накрест.
X лучше Y. В одном 4-полюсном штекере оба направления делят землю — наводка ближнего конца неизбежна. Поэтому базовый режим — полудуплекс с подтверждением: одна сторона передаёт кадр, другая подтверждает, направление меняется. В приёмнике в каждый момент только одно направление. Эхо из комбинированного разъёма перестаёт быть задачей адаптивного эхоподавления.
Длина кабеля — не больше двух метров. На такой длине ёмкость линии не режет полосу до 20 кГц. Ограничители другие: кодеки, АРУ, наводки в разветвителе, контур заземления. Две машины от разных розеток дают фон 50 Гц по общему проводу. Лечится питанием от одного блока, разделительными трансформаторами или питанием сервера от ноутбука по USB — USB тогда только питание, не канал.
Ещё три бытовых ловушки, без которых FSK не собирается.
CTIA и OMTP меняют местами микрофон и землю. Не тот Y‑разветвитель — «микрофон молчит». Современный ноутбук почти всегда CTIA.
Уровни. Выход наушников — доли вольта, полный размах mic‑входа — десятки милливольт. В сторону микрофона нужен делитель (типично килоом в последовательной цепи и сотни ом в шунт, подбирается замером). Иначе клиппинг и гармоники во второй тон: детектор видит оба сразу.
АРУ и шумоподавление. В Windows снимаются «улучшения» микрофона: АРУ, подавление шума и эха. На Linux агент берёт ALSA hw: / plughw:, не PulseAudio и не PipeWire. Чистый тон для шумодава — стационарный шум. Он его давит. Уровни — amixer, не ползунок микшера в GUI.
Полный дуплекс остаётся возможным после замеров на чистой X‑схеме. Кадрирование для этого менять не надо: убрать паузу между циклами и развести счётчики seq по направлениям.
4. Тон как с кассеты, не «свой модем с нуля»
Ориентир был прямой: как с кассеты на Z80 и как по факсу. Практически это FSK/AFSK, а не новый модулятор «потому что интересно». Готовые ориентиры —minimodem и Dire Wolf; свой код — потому что на сервере в аварии нет ни того, ни другого, и ставить их из сети нельзя.
Канал — CPFSK, тон Bell 202, кадр 8N1: старт, 8 бит младшим первым, стоп. Стартовый и стоповый биты на каждом байте дают точки восстановления тактов: два независимых кварца не накапливают уход на длине кадра. Преамбула — чередование 0x55 вне 8N1, чтобы захватить такты и уровень. Полезная нагрузка до 240 байт, контрольная сумма CRC-16/CCITT‑FALSE. Непрерывность фазы обязательна: разрыв даёт щелчок, который лезет в оба тона.
Три режима:
|
Режим |
Бод |
Тон, Гц |
Зачем |
|---|---|---|---|
|
|
300 |
1200 / 2200 |
захват канала, HELO |
|
|
1200 |
1200 / 2200 |
рабочая консоль |
|
|
2400 |
1400 / 3400 |
выключен |
Почему нельзя просто «включить 2400 на тех же тонах». У некогерентного FSK разнос тонов должен быть порядка скорости. На 2400 бод пара 1200/2200 (разнос 1000 Гц) неразличима: на бит 2200 Гц приходится 0,92 периода, детектор не накапливает энергию. Для 2400 нужна другая пара — 1400/3400. Верхний тон уже у края телефонной полосы и у края многих кодеков. АРУ и фаза могут убить его, даже если спектр «как будто есть».
Поэтому stretch в 1.0.0 выключен флагом. Рукопожатие его не предлагает. Включать — только после замера BER на конкретном кабеле и явного «да». На 1200 бод интерактивная консоль уже ходит. Этого хватает для задачи «команда в секунду».
Рукопожатие всегда начинается с probe: медленно, но с запасом по разносу (примерно 21 дБ полезного отклика к перетеканию против 14 дБ на 1200). Поймали HELO — поднялись в base. Если в probe ответа нет, клиент повторяет тот же HELO уже на 1200: после удачного сеанса агент остаётся слушать base и 300 бод просто не слышит.
5. Протокол: один мастер
Роли несимметричны. Клиент — единственный, кто начинает передачу. Агент сам в эфир не лезет. Коллизий нет, при отладке всегда понятно, кто должен молчать.
клиент REQ ──► агент
клиент ◄── RESP
клиент ACK ──► агент
(тишина GAP)
Цикл полудуплекса: REQ → RESP → ACK → GAP.
Кадр: преамбула, sync 0x7E, тип, seq, длина, payload, CRC. Повтор того же seq не запускает команду второй раз: отдаётся закэшированный ответ. Иначе на шумном кабеле netplan apply уехал бы дважды.
Обратно едет не «вывод команды», а снимок терминала: строки × столбцы, курсор, флаги. Первый кадр и сброс — полный снимок. Дальше дельта по изменившимся строкам. Если 24×80 после motd не сжимается в 240 байт — нарезка SCREEN_PART / SCREEN_MORE. Без этого целевая задержка одного обмена недостижима: полный несжимаемый экран на 1200 бод — уже не «секунда».
Код возврата команды канал отдельно не передаёт. Видна только сетка. Поэтому после каждого шага надо читать экран, а не ждать exit code. Это неудобно для привыкших к SSH. Для аварии — честно: vim и htop так и живут, картинкой.
Файлы в обе стороны — отдельные типы кадров (FILE_OPEN / FILE_DATA / FILE_CLOSE и зеркало на скачивание), CRC-32 на файл. Конфиг на килобайт реалистичен. Восемь килобайт — это уже десятки полудуплексных циклов, порядка полутора минут. Архив на мегабайты — нет, и не обещали.
Версии протокола при HELO сверяются жёстко: несовпадение — ошибка, не тихая деградация. Лучше отказаться сразу, чем два конца будут «почти совместимы» и молча портить кадры.
6. Две программы, ноль pip на сервере
Один пакет xconn_channel, Python 3.10+, стандартная библиотека. На Windows лаунчер —py (python из Microsoft Store — заглушка, его нет). На сервере в момент аварии нет ни pip, ни зеркала пакетов, ни компилятора.
Клиент открывает звук через winmm. Агент — через ALSA, минуя Pulse/PipeWire. На живом Realtek ALC897 чистый моно hw:1,0 не открывается (Channels count non available). Рабочий путь —plughw:1,0 или стерео с даунмиксом.
Проверка без кабеля:
py -m xconn_channel loopback -c "echo hello"
Оба конца в одном процессе. Так гоняется текстовый терминал VT100, нарезка экрана и файлы, до того как в дело вступают кодеки.
Установка на сервер — сценарий с курицей и яйцом: канал ещё не работает, агента некуда скачать. На ноутбуке (интернет есть — можно скачать) пишется флешка:
py -m xconn_channel stick E:
На носитель попадают пакет, install.sh, unit systemd, автономный Linux CPython (x86_64 и aarch64) и alsa-utils. На сервере сети нет:
sudo sh install.sh
sudo systemctl restart xconn-agent
-
sudo sh install.sh— копирует агент в/opt/x_connector, ставит офлайн Python и ALSA, заводит пользователяxconnи sudoers. -
sudo systemctl restart xconn-agent— поднимает новый процесс.startпосле обновления файлов оставляет в памяти старыйvt100.py.
Sudo без пароля: одна строка xconn ALL=(root) NOPASSWD:ALL. Defaults:xconn !requiretty на Ubuntu 26.04 писать нельзя — visudo 1.9.17+ не знает эту настройку и откатывает весь файл. Я так один раз снес рабочий sudoers: установщик «упал», а вместе с ним и право чинить сеть без пароля.
Агент не ходит в сеть даже «за зависимостью». IPAddressDeny=any в unit — не декорация.
7. Что сломалось на первом кабеле
Первый живой прогон 22 сентября 2026. Приёмка первой сборки 25-го. Между ними канал «в принципе пищал», но консоли, которой можно верить, ещё не было.
Ниже — семь граблей. Каждая выглядела как «протокол мёртв», пока не находился бытовой виновник.
HDMI вместо наушников
На ноутбуке карта 0 — выход в монитор. Команда с --playback 0 уходит в HDMI. В кабеле тишина. Агент слушает line‑in и правильно молчит: энергии нет, преамбулы нет. Со стороны клиента это «нет ответа на HELO», то есть «наверное CRC, боды, баг».
Лечится одной командой, которую надо запустить до теории модуляции:
py -m xconn_channel devices
Рабочая пара — --capture 0 --playback 1. Карта 0 пишет звук в кабель, карта 1 его не видит, потому что это выход.
ALC897 не бывает моно
Агент на сервере открывал hw:1,0 с одним каналом. Realtek отвечал Channels count non available и падал. В /proc/asound/pcm аналог — карта 1, HDMI снова карта 0: та же ловушка, что на ноутбуке.
Рабочий путь — plughw:1,0. Плагин ALSA сам ставит стерео и даунмикс. Отдельно: без пакета alsa-utils нет aplay/arecord, агент выходит с кодом 2. apt на сломанной сети нет. Deb‑пакеты лежат на флешке.
Рукопожатие уехало в кэш
После удачного сеанса агент остаётся слушать 1200 бод. Новый клиент по спецификации начинает с 300. Эфир мёртвый. Клиент теперь повторяет тот же HELO уже в base.
Вторая половина той же ошибки тоньше. Кадр HELO нёс seq=0. У агента с прошлой сессии в кэше как раз лежал ответ на ноль. Протокол честно отдал кэш: «этот seq уже был». Клиент ждал HELO, получал старый NAK или кусок экрана и решал, что сервер с ума сошёл.
Лечение: HELO не участвует в кэше ответов. Тип кадра важнее номера.
Экран не влезал в кадр
Payload максимум 240 байт. Свежий логин, motd Ubuntu, сетка 24×80, zlib почти не сжал — NAK «не влезает». Казалось, что 1200 бод «не тянут консоль». Тянут, если резать: SCREEN_PART и запрос следующего куска. Старый агент без нарезки по‑прежнему отвечает nak=0x03. Клиент тогда жмёт окно до 8×32 — читаемо, но уже не «как монитор».
В сетке светится 3008;start=
systemd пишет в терминал OSC 133 — semantic prompt. Эмулятор не знал ESC ] … BEL и выплёвывал хвост в клетки. На экране вместо приглашения — мусор 3008;start=. Команды при этом исполнялись. Верить картинке было нельзя.
VT100 теперь глотает OSC до BEL или ST (ESC ). Терминатор BEL не поднимает флаг «звонок»: это не Ctrl‑G пользователя.
Снимок посреди ping
Помпа (цикл, который читает PTY и снимает сетку) забирала PTY через 80–250 мс и снимала сетку. ping -c 2 приезжал без статистики. netplan apply — без хвоста. Оператор думал, что команда зависла или канал отрезал вывод.
На Linux у PTY есть foreground: TIOCGPGRP. Пока в оболочке сидит не сама оболочка, помпа ждёт (с потолком, чтобы мёртвый процесс не держал эфир вечно). Клиент на команде и клавише держит длинный таймаут несущей: ответ после ping едет дольше, чем после echo.
Длинный ввод упирался во второй потолок — 240 байт. PowerShell на ноутбуке ещё и ломал кавычки. Лечение: клиент сам режет CMD на куски, перевод строки — только на последнем. Для netplan я один раз уехал через файл и sudo cp.
Файлы новые, процесс старый
Флешку переписали, install.sh отработал, systemctl restart сообщил, что служба уже запущена. На диске — новый vt100.py, в памяти — вчерашний. OSC продолжал течь. Нужен restart. После этого приёмка: один установщик с клавиатуры, дальше только кабель. ip, ss, journalctl, sudo, правка netplan, запись файла, reload службы. Монитор сервера больше не нужен.
Каждый раз я был уверен, что сломан протокол, и каждый раз оказывалось, что виноват провод или настройка.
Если после обновления «как будто откатилось» — сначала
systemctl restart xconn-agent, потом теория протокола.
8. Что умеет 1.0.0 и чего в нём нет
1.0.0 — не бета. Приёмка первой сборки пройдена. Это релиз консоли починки сети, не «модема на все случаи».
Входит. Полудуплекс 1200 бод, VT100-сетка, файлы в обе стороны, флешка без интернета на сервере, sudo без пароля, живой тракт Windows winmm ↔ Linux ALSA.
Не входит — следующие фазы или сознательный отказ, не дыры релиза:
-
формальный отчёт BER/FER по этому кабелю (
py tools/ber.py --liveуже есть;--selftestбез железа зелёный); -
режим
stretch2400 бод; -
HID и слепой подъём канала;
-
шифрование, полный дуплекс, SSH и IP поверх звука;
-
офлайн‑LLM на ноутбуке.
Мозг в первой сборке — облачный ИИ на ноутбуке с интернетом. Клиент — управляемая программа: отправить, дождаться сетки, прочитать. Не автономный бот.
Количественный BER и качественная приёмка — разные вещи. Консоль может чинить netplan, а FER при этом быть ненулевым: протокол повторяет кадр. Пока нет отчёта --live, нельзя честно сказать «запас канала такой‑то» и нельзя включать 2400. Команда замера есть. Цифр с этого кабеля в репозитории нет — и в статье я их не выдумаю.
9. Как повторить
Нужны две машины, два аудиопровода не длиннее 2 м, флешка, Python 3.10+ на ноутбуке.
Без кабеля — убедиться, что пакет жив:
git clone https://github.com/DmNep/x_connector.git
cd x_connector
py -m unittest discover -s tests
py -m xconn_channel loopback --repl
По кабелю.
-
Отключить на ноутбуке улучшения микрофона.
-
Посмотреть устройства:
py -m xconn_channel devices. Не играть в HDMI. -
Записать флешку:
py -m xconn_channel stick E:. -
На сервере с монитором смонтировать носитель и поставить агент:
sudo sh install.sh
sudo systemctl restart xconn-agent
-
sudo sh install.sh— автономная установка из содержимого флешки, без сети. -
sudo systemctl restart xconn-agent— запуск уже нового кода.
-
Соединить X‑схемой. На ноутбуке:
py -m xconn_channel client --capture N --playback M --repl. -
Дождаться сетки 24×80. Дальше как в обычной консоли: команда,
.key ctrl-c,.put/.get,.ping.
Подробности тракта, калибровка probe.py и таблица поломок — в мануале. Спецификация кадров — в protocol.md.
Не ходите с сервера в интернет «доустановить зависимость». Если утилиты нет — это ограничение среды. Канал его не обходит.
10. Заключение
x_connector не заменяет SSH и не делает из звуковой карты Ethernet. Он лежит рядом, когда SSH уже смотрит в потолок: сломанная маршрутизация, жирный VPN, пустой DNS, закрытый файрвол.
Решения, которые держат проект маленьким: полудуплекс, один мастер, сетка вместо скриншота, stdlib, флешка вместо apt, никакого IP в звуке. Скорость скромная. Зато канал не спрашивает разрешения у сети, которую вы чините.
Грабли из раздела 7 дешевле описать, чем героически «изобретать модем». Половина «мёртвого эфира» была HDMI, АРУ и start вместо restart.
После того как сеть ожила, провода можно вынуть. Можно оставить. Второй раз собирать установку с нуля не хочется — поэтому агент переживает перетыкание кабеля, а клиент снова начинает с HELO.
Код: github.com/DmNep/x_connector, пакет xconn_channel 1.0.0.
Автор: DmNep


