惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
Cisco Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
IT之家
IT之家
博客园 - 【当耐特】
V
V2EX
博客园_首页
T
Tailwind CSS Blog
Last Week in AI
Last Week in AI
G
Google Developers Blog
The Last Watchdog
The Last Watchdog
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 司徒正美
N
Netflix TechBlog - Medium
F
Fortinet All Blogs
Know Your Adversary
Know Your Adversary
S
Schneier on Security
V
Vulnerabilities – Threatpost
T
The Exploit Database - CXSecurity.com
Vercel News
Vercel News
量子位
G
GRAHAM CLULEY
T
Threatpost
D
Darknet – Hacking Tools, Hacker News & Cyber Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Cybersecurity and Infrastructure Security Agency CISA
S
Security @ Cisco Blogs
B
Blog
Stack Overflow Blog
Stack Overflow Blog
T
Tor Project blog
A
About on SuperTechFans
博客园 - 叶小钗
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
月光博客
月光博客
S
Securelist
博客园 - 聂微东
Cloudbric
Cloudbric
N
News and Events Feed by Topic
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
H
Help Net Security
N
News | PayPal Newsroom
P
Privacy & Cybersecurity Law Blog
Schneier on Security
Schneier on Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
W
WeLiveSecurity
Martin Fowler
Martin Fowler
K
Kaspersky official blog
S
Security Affairs
TaoSecurity Blog
TaoSecurity Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Доступ к VirtualBox и Hyper-V без агента в гостевой ОС — на примере клиента EvertyDesk Lite
vaalimusic · 2026-06-26 · via Все публикации подряд на Хабре

Сложный

8 мин

39

В какой-то момент возникла задача: дать админу возможность подключаться к виртуальным машинам прямо на хосте, к которому он уже подключён. Зашёл на физический хост — видишь его VM как живые превью, кликнул — работаешь в консоли VM. И всё это без установки агента внутрь гостевой ОС.

Звучит просто. На практике это вылилось в путешествие через три RDP-сервера разной степени «соответствия стандарту», панику внутри чужой библиотеки и один таймаут, который перевернул всю архитектуру. Рассказываю с кодом и набитыми шишками.

VM Секция - Rust+EGUI

VM Секция - Rust+EGUI

На практике — это путешествие через три RDP-сервера разной степени «соответствия стандарту», панику внутри чужой библиотеки и один таймаут, который перевернул всю архитектуру. Рассказываю с кодом.

Зачем agentless

Классический сценарий: у клиента сломалась сеть в гостевой ОС, или VM ещё на этапе установки, или это Linux без RDP. Агента внутрь не поставить — сети нет. Но гипервизор-то VM видит. И VirtualBox, и Hyper-V умеют отдавать экран VM через свои механизмы. Значит, можно подключиться «снаружи», со стороны хоста, через API гипервизора.

Архитектурно у нас получилось два провайдера: VirtualBox - VRDE (встроенный RDP-сервер) поверх TCP+TLS и Hyper-V Enhanced Session (RDP) через брокер vmms

Оба, как видите, в итоге говорят на RDP. Это и стало ключом к пере использованию кода.

VirtualBox: VRDE + ironrdp

У VirtualBox есть VRDE (VirtualBox Remote Display Extension) — встроенный RDP-сервер. Включается одной командой:

pub fn enable_vrde(uuid: &str, port: u16, vm_running: bool) -> bool {
    let vbm = vboxmanage()?;
    if vm_running {
        vbm_run(&vbm, &["controlvm", uuid, "vrdeport", &port.to_string()]);
        vbm_run(&vbm, &["controlvm", uuid, "vrdeproperty", "Security/Method=tls"]);
        vbm_run(&vbm, &["controlvm", uuid, "vrde", "on"]);
    }
    // ...
}

Дальше нужен RDP-клиент. Городить FFI к FreeRDP не хотелось — нашли ironrdp чистую Rust-реализацию RDP от Devolutions. Подключаемся к 127.0.0.1:port, делаем X.224-негоциацию, поднимаем TLS, и крутим ActiveStage, который декодирует bitmap-апдейты в RGBA:

let outputs = active.process(&mut image, action, &frame)?;
for output in outputs {
    match output {
        ActiveStageOutput::GraphicsUpdate(_) => had_update = true,
        ActiveStageOutput::PointerBitmap(p) => { cursor_shape = Some(p); }
        // ...
    }
}
if had_update {
    send_frame(&image, &cursor_shape, cur_x, cur_y, &frame_tx);
}

Красиво на бумаге. А теперь — реальность.

Шишка N1: VirtualBox шлёт пустой Font Map

Первое же подключение упало на финализации:

FontPdu decode: not enough bytes provided to decode: received 0 bytes, expected 8 bytes

VirtualBox VRDE завершает фазу подключения пакетом Font Map с нулевым телом. По спецификации там должно быть минимум 8 байт, и ironrdp честно отказывается это парсить. Пришлось написать обёртку над финализацией, которая ловит именно эту ошибку и считает её успешным завершением:

match single_sequence_step(framed, &mut connector, &mut buf) {
    Ok(()) => {}
    Err(e) => {
        // VirtualBox VRDE присылает пустой FontMap — для ironrdp это ошибка,
        // но фактически это конец финализации. Реконструируем результат.
        if error_chain_contains(&e, "FontPdu") {
            return Ok(reconstruct_connection_result(saved));
        }
        return Err(e);
    }
}

Важный момент, который мы поняли позже: мы скачали официальный референсный клиент ironrdp-viewer и натравили его на ту же VM. Он упал на том же месте. То есть это не наш косяк - VirtualBox реально нарушает протокол, и даже эталонный клиент Devolutions без обходного пути к нему не подключается. Это нас сильно успокоило.

Шишка N2: чёрный экран и «магия» сжатия

Подключение прошло - а экран чёрный. При этом счётчик графических апдейтов растёт. Декодер работает, но рисует пустоту.

Я долго копал и нашел в исходниках ironrdp-session:

// active_stage.rs
let bulk_decompressor = connection_result.compression_type.and_then(|ct| {
    BulkCompressor::new(...)
});

Мы передавали compression_type: None — мол, сжатие не нужно. Но VirtualBox всё равно слал сжатые Fast-Path обновления. А раз декомпрессора нет — сжатые байты молча скармливались декодеру как несжатые. Ни ошибки, ни картинки.

Лечится одной строкой в конфиге:

// VirtualBox VRDE шлет bulk-compressed Fast-path графику независимо от того,
// что мы объявляем. Без compression_type ironrdp не создаёт BulkCompressor,
// и сжатые апдейты тихо превращаются в мусор.
compression_type: Some(CompressionType::K64),

Картинка появилась.

Шишка N3: декодер падает в панику внутри библиотеки

Картинка появилась, но каждые несколько секунд сессия рвалась. Долго не могли понять причину — поток просто «умирал». Добавили перехват паники прямо в рабочем потоке:

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    vrde_thread(...);
}));
if let Err(payload) = result {
    let msg = downcast_panic_message(&payload);
    log_line(&format!("session thread PANICKED: {msg}"));
}

И увидели правду:

PANIC at ironrdp-session/src/image.rs:694:
index out of bounds: the len is 8294400 but the index is 8297092

Паника внутри библиотеки. Функция применения битмапа rect_fits() проверяет, что объявленный прямоугольник обновления влезает в границы экрана — но не проверяет, что реальных данных битмапа не больше, чем заявлено в прямоугольнике. VirtualBox иногда присылает строк данных больше, чем декларирует, chunks_exact нарезает лишние строки, и индекс вылетает за пределы массива.

Проверили свежий код на GitHub — баг не исправлен. Поэтому завендорили крейт в проект и добавили защиту во все функции применения битмапа:

let rectangle_height = usize::from(update_rectangle.height());
bgr24
    .chunks_exact(rectangle_width * SRC_COLOR_DEPTH)
    .rev()
    .take(rectangle_height)   // <- наш фикс: не вылезаем за объявленную высоту
    .enumerate()
    .for_each(|(row_idx, row)| { /* ... */ });

Подключается через [patch.crates-io] в Cargo.toml:

[patch.crates-io]
ironrdp-bulk = { path = "vendor/ironrdp-bulk" }
ironrdp-session = { path = "vendor/ironrdp-session" }

Это уже третий баг на стороне VirtualBox/ironrdp, который мы нашли и обошли. Mstsc.exe от Microsoft при этом работал с VRDE идеально — потому что Microsoft за 25 лет научил свой RDP-клиент терпеть кривые сторонние серверы. Молодая независимая реализация на Rust такого запаса толерантности пока не накопила.

Изящное решение: как понять, что поток умер

Отдельная история — детектор «зависаний». Мы перепробовали кучу подходов на таймерах («нет кадра N миллисекунд → реконнект»), и все они давали ложные срабатывания: статичный экран легитимно не присылает кадры, и отличить «тихо, потому что нечего обновлять» от «тихо, потому что сломалось» по времени невозможно.

Правильное решение оказалось без таймеров вообще. Когда рабочий поток завершается (по любой причине — ошибка, паника, штатный выход), его mpsc::Sender дропается, и канал закрывается. Это 100% надёжный сигнал:

pub enum Poll<T> {
    Item(T),
    Empty,
    Dead,   // поток завершился — канал закрыт
}

impl<T> From<Result<T, mpsc::TryRecvError>> for Poll<T> {
    fn from(r: Result<T, mpsc::TryRecvError>) -> Self {
        match r {
            Ok(v) => Poll::Item(v),
            Err(mpsc::TryRecvError::Empty) => Poll::Empty,
            Err(mpsc::TryRecvError::Disconnected) => Poll::Dead,
        }
    }
}

Живой поток никогда не даст Dead. Никаких эвристик, никаких ложных срабатываний — реконнект происходит ровно тогда, когда поток реально умер, и мгновенно. Один тип, исчерпывающий match, и невозможно по ошибке прочитать «мёртв», не обработав при этом последнее реальное сообщение.

И ещё одна тонкость, которую легко упустить: таймаут стоял только на чтение сокета, но не на запись. Зависший write_all (переполненный буфер отправки на полумёртвом соединении) блокировал поток навсегда — и ни один детектор это не ловил, потому что поток физически не выполнял код. Одна строчка спасла:

let _ = spy.inner.get_ref().set_write_timeout(Some(Duration::from_secs(5)));

Hyper-V: поворот, который всё упростил

С VirtualBox разобрались. Берёмся за Hyper-V Enhanced Session — это тоже RDP, но «поверх VMBus». Логично предположить: гость слушает RDP на Hyper-V-сокете (AF_HYPERV), подключаемся напрямую к VmId + ServiceId.

Написали транспорт через AF_HYPERV, подключаемся... и получаем:

HV-RDP: VMBus connect: ... (os error 10060)

Таймаут. 30 секунд впустую. Прямого hv_sock RDP-листенера в госте по этому ServiceId нет.

Покопались, как на самом деле работает vmconnect.exe, и оказалось красивее. Хост Hyper-V запускает брокер VM-подключений (vmms), который слушает обычный TCP на 127.0.0.1:2179. Клиент:

  1. Открывает обычный TCP к этому порту.

  2. Шлёт первым же пакетом Preconnection Blob V2 с GUID нужной VM — брокер по нему понимает, в какую VM проксировать.

  3. Дальше — совершенно стандартный RDP-хендшейк; брокер сам прокидывает его в RDP-сервер гостя через VMBus.

Я бы сказал что транспорт — обычный TCP+TLS, как у VirtualBox, и единственное отличие — один служебный пакет в начале. А ironrdp уже умеет кодировать Preconnection Blob:

let mut tcp = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;

let pcb = PreconnectionBlob {
    version: PcbVersion::V2,
    id: 0,
    v2_payload: Some(vm_guid.clone()),  // GUID нужной VM
};
tcp.write_all(&ironrdp_core::encode_vec(&pcb)?)?;

// дальше — обычный ironrdp handshake, как для VRDE
let mut connector = ClientConnector::new(config, client_addr);
let mut framed = Framed::new(tcp);
let should_upgrade = connect_begin(&mut framed, &mut connector)?;

Главный профит: один движок на оба провайдера

Поскольку оба провайдера после установки соединения говорят на чистом RDP, весь движок переиспользуется. VirtualBox и Hyper-V отличаются только способом получить байтовый поток (TCP к VRDE-порту против TCP к брокеру + preconnection blob). Всё, что после, — общее: декод кадров, кодирование ввода, отрисовка.

Helperы ввода в ironrdp generic над S: Read + Write, так что их хватило сделать pub(crate) и звать из обоих модулей:

// hyperv_rdp.rs переиспользует ровно те же функции, что и vbox_rdp.rs
use crate::vbox_rdp::{
    Poll, VrdeCmd, char_to_rdp_scancode, composite_cursor,
    emit_key_event, emit_mouse_event, emit_unicode_event,
    is_ignorable_pdu_error, is_transient_read_error, sanitize_desktop_size,
};

Даже команды ввода (VrdeCmd::MouseMoveKeyDownMouseWheel, …) общие — UI собирает их одним блоком и шлёт в ту сессию, которая активна:

for cmd in input_cmds {
    if let Some(vrdp) = &self.vbox_vrde_session {
        vrdp.send(cmd);
    } else if let Some(rdp) = &self.hyperv_rdp_session {
        rdp.send(cmd);
    }
}

Чему научились

  1. «Стандарт» — это очень растяжимо. VirtualBox VRDE нарушает RDP в трёх местах (пустой FontMap, лишние строки битмап, усечённые control-PDU). Эталонный клиент Devolutions падает на том же. Реальный мир грязнее спецификации, и зрелость клиента — это в основном накопленная толерантность к чужим багам.

  2. Не бойтесь вендорить и патчить зависимости. Баг с паникой в ironrdp-session нельзя было обойти из своего кода — только форк. [patch.crates-io] делает это безболезненно.

  3. Сигнал должен нести информацию. Таймер «нет кадра N мс» в принципе не отличает тишину от поломки. Закрытие канала при смерти потока — отличает на 100%. Иногда правильное решение — не крутить чувствительность датчика, а сменить датчик.

  4. Неверная архитектурная гипотеза стоит дёшево, если проверять рано. Один таймаут 10060 сэкономил нам недели возни с AF_HYPERV и привёл к более простому и правильному решению через брокер vmms.

В итоге получили agentless-доступ к VM обоих гипервизоров одним движком, без агентов в гостях, прямо из remote-desktop клиента. Hyper-V-путь оказался даже стабильнее — Windows-гость отдаёт «чистый» RDP без тех болячек, что мы лечили у VirtualBox.

Фиксы — можно забрать в апстрим

Наши патчи к ironrdp — это не хаки «лишь бы у нас заработало», а исправления реальных багов, которые до сих пор не исправлены в апстриме:

  • защита от выхода за границы массива в apply_*_bitmap (ironrdp-session/src/image.rs) — паника прямо в библиотеке при «лишних» строках битмапа;

  • терпимость к усечённым/нестандартным control-PDU при финализации и реактивации;

  • самовосстановление MPPC-декодера в ironrdp-bulk.

Все три бага я описал выше достаточно подробно, чтобы воспроизвести и поправить — где именно ломается, при каких условиях и почему. Так что если кто-то из сообщества хочет довести их до pull request в IronRDP — берите и вносите сами, описания для этого хватает. Сам репозиторий я не выкладываю, но если нужны готовые патчи под конкретную версию крейтов — напишите в личку, пришлю diff.

Баги общие: от того, что они уедут в апстрим, выиграют все, кто пробует подключаться ironrdp к VirtualBox VRDE или другим «не-эталонным» RDP-серверам.

На этом всё. Если статья оказалась полезной или у вас есть свои истории борьбы с «не-эталонными» RDP-серверами — буду рад обсуждению в комментариях.

Спасибо, что дочитали.