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

推荐订阅源

L
LangChain Blog
有赞技术团队
有赞技术团队
博客园_首页
IT之家
IT之家
爱范儿
爱范儿
量子位
小众软件
小众软件
Jina AI
Jina AI
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
The Cloudflare Blog
博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
雷峰网
雷峰网
V
Visual Studio Blog
博客园 - Franky
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
美团技术团队
Last Week in AI
Last Week in AI
S
SegmentFault 最新的问题

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания
Как пробросить свой UDP-транспорт через RustDesk без патч...
vaalimusic · 2026-06-26 · via Все публикации подряд на Хабре

Сложный

4 мин

0

Привет, Хабр!
Есть RustDesk-инфраструктура: rendezvous-сервер (hbbs) для NAT traversal и relay (hbbr) для проброса трафика, когда P2P не получился. И есть свой UDP-транспорт реального времени для видео (у меня это EVRT), который хочется гнать напрямую между пирами, минуя relay — ради задержки. Вопрос: как двум пирам договориться о прямом UDP-канале, если они общаются только через RustDesk?
Ответ короткий: RustDesk relay — это «тупая труба». Он передаёт зашифрованные PeerMessage между пирами и не парсит их содержимое. Значит, в эти сообщения можно подложить свои данные — и сервер прозрачно их пробросит, ничего не зная про твой протокол.

Общая схема

Общая схема

Шаг 1. Расширяем Misc своими полями

Desk-сообщения — это protobuf. Внутри PeerMessage есть Misc с oneof-полем union. Туда и добавляем свои варианты — с большими tag-номерами, заведомо вне диапазона, который использует апстрим RustDesk:

// rustdesk_proto.rs — наш форк .proto
oneof union {
    // ... стандартные поля RustDesk (tag 1..40) ...

    /// EVRT: хост сообщает свой UDP-порт для прямого стриминга.
    /// tag 100 — вне диапазона стандартных RustDesk полей.
    #[prost(uint32, tag = "100")]
    EvrtUdpPort(u32),

    /// EVRT: список IP:порт кандидатов хоста (LAN + VPN + внешний).
    /// Формат: "ip1:port,ip2:port,...". Клиент пробует каждый (mini-ICE).
    #[prost(string, tag = "101")]
    EvrtEndpoints(String),
}

Почему tag 100+ — критично. protobuf — формат с прямой совместимостью: неизвестные поля просто игнорируются при декодировании. RustDesk-сервер релеит наш PeerMessage как непрозрачный блоб (он его даже не расшифровывает — это E2E между пирами), но если бы и парсил — поля с tag 100/101 он бы пропустил, не сломавшись. А мы на своей стороне (оба пира — наш клиент) их видим. Берёшь незанятый диапазон — и расширяешь протокол, не трогая ни сервер, ни апстрим.

Шаг 2. Хост анонсирует свой UDP-порт

Хост уже подключён к клиенту через RustDesk (relay или P2P-TCP — неважно). Он биндит свой EVRT UDP-сокет и отправляет порт обычным PeerMessage по тому же каналу:

fn evrt_port_message(port: u16) -> PeerMessage {
    PeerMessage {
        union: Some(peer_message::Union::Misc(Misc {
            union: Some(misc::Union::EvrtUdpPort(port as u32)),
        })),
    }
}

Это уходит в тот же зашифрованный поток, что и кадры/ввод. Для RustDesk-сервера это просто очередной байтовый блоб от хоста к клиенту.

Шаг 3. Клиент собирает адрес и пробует P2P до relay

Тут вся соль. У клиента уже есть IP пира — его дал rendezvous-сервер в PunchHoleResponse.socket_addr при пробивке NAT. А порт только что прилетел в Misc{EvrtUdpPort}. Складываем — получаем полный адрес для прямого UDP:

Some(misc::Union::EvrtUdpPort(port)) => {
    let p = (*port).min(65535) as u16;
    if p > 0 {
        // IP уже есть от punch-hole, порт — отсюда. Адрес собран за один запрос.
        *evrt_port_out = Some(p);
    }
}

И запускаем попытку прямого EVRT-соединения раньше, чем начнём гнать видео через relay:

// transport.rs — EVRT адрес собран: IP от hbbs punch-hole + порт из Misc.
if let Some(host_addr) = evrt_host_addr {
    thread::spawn(move || {
        let udp = std::net::UdpSocket::bind("0.0.0.0:0").unwrap();
        crate::evrt_client::try_evrt_before_relay(
            &Arc::new(udp),
            host_addr,         // <- IP:port пира для прямого UDP
            evrt_token,
            &events,
            evrt_stop,
            ull,               // ultra-low-latency при 60fps
        );
    });
}

try_evrt_before_relay шлёт UDP-пробу на собранный адрес. Если прошла — видео идёт напрямую, relay используется только под control-канал (мышь/клава/clipboard — там байт мало). Если не прошла за таймаут — спокойно остаёмся на relay, ничего не сломалось. То есть это оппортунистический апгрейд: пробуем лучшее, откатываемся к рабочему.

Зачем второе поле — EvrtEndpoints

Один порт хорош, пока у хоста один IP. Но если хост за VPN или мультихоумный — у него несколько адресов, и угадать «правильный» нельзя. Поэтому есть второй вариант: хост шлёт список кандидатов ip1:port,ip2:port,..., а клиент пробует каждый — мини-ICE на минималках:

Some(misc::Union::EvrtEndpoints(list)) => {
    for addr in parse_evrt_endpoints(list) {
        if !evrt_candidates_out.contains(&addr) {
            evrt_candidates_out.push(addr);   // потом try_evrt_candidates(...)
        }
    }
}

Тот же приём, та же «тупая труба» — просто строка вместо числа.

Зачем это нужно.

В RustDesk уже есть готовая и надёжная сигнальная инфраструктура: rendezvous, relay, NAT punch-hole, E2E-канал между пирами. Писать всё это заново ради своего видеотранспорта — долго, дорого и рискованно. Гораздо практичнее использовать RustDesk как «служебную шину»: пусть он по-прежнему устанавливает соединение, договаривается о пирах и держит control-канал, а тяжёлый поток видео мы при первой возможности выносим в свой UDP-транспорт. Так мы получаем лучшее из двух миров: совместимость со стоковым RustDesk-сервером и возможность ускорять медиа-трафик без форка всей инфраструктуры.

Итого

Чтобы пробросить свой транспорт через чужую сигнальную инфраструктуру, не нужно её патчить. Нужно:

  1. Найти у неё канал, который релеит данные прозрачно (у RustDesk это E2E-PeerMessage).

  2. Расширить сообщение своими полями в незанятом диапазоне tag'ов (protobuf простит неизвестные поля).

  3. Сложить то, что уже знаешь (IP от punch-hole), с тем, что передал (порт), — и пробовать прямой путь оппортунистически, с откатом на relay.

Сервер RustDesk при этом остаётся стоковым — он даже не подозревает, что внутри его трубы поехал чужой UDP-транспорт.

Спасибо за внимание. Если интересны детали самого EVRT-протокола (адаптивная буферизация, система давления) — это тема для отдельной статьи.