RustDesk Pro в России не купить. После долгих лет администрирования мы собрали своё честное решение

от автора

Статья человека, который десять лет администрировал чужие компьютеры, а теперь делает то, чем хочет администрировать сам.

Берем официальный RustDesk (AGPLv3), не делаем форк, патчим его на лету в GitHub Actions при каждой сборке клиента. Поверх — российская инфраструктура: серверы в РФ, оплата по счёту юр.лицам, корпоративный SSO через Active Directory и Яндекс ID, защита от мошенничества на Android.

Меня зовут Артур Валиев. Я делаю не «решение для импортозамещения с сертификацией ФСТЭК» ради закупок. Просто работающий продукт, который я бы сам хотел использовать десять лет назад, когда сидел на саппорте у клиентов.

Почему я вообще это делаю

Десять лет я был эникейщиком, потом сисадмином, потом инженером поддержки в IT-аутсорсе. Прошёл TeamViewer, AnyDesk, LiteManager, AeroAdmin, Ammyy, всё что вы видите в списке. У каждого свои тараканы:

  • AnyDesk считает вас коммерческим пользователем, если вы помогли маме настроить принтер дважды

  • LiteManager — пытались, но интерфейс из 2008 года плюс лицензия по штукам, неудобно

  • RuDesktop — про них отдельно ниже

Про честность с AGPLv3 (и почему я не «как RuDesktop»)

Когда я начал, посмотрел российских конкурентов. Один из заметных — RuDesktop. У них на сайте красивые бейджи «Реестр росПО», «Сертификация ФСТЭК», и публичные заявления про «собственную разработку». При этом — это форк RustDesk без публикации исходников, что прямо нарушает AGPLv3.

AGPL — это не «можно посмотреть и забыть». Это: используешь — публикуй изменения. Раздаёшь как сетевой сервис — публикуй. Любой может потребовать исходники, если узнает про использование.

Я не хочу так. Это краткосрочно выгодно (никто не проверит при тендере), но долгосрочно — тикающая бомба. Когда RustDesk проснётся и подаст в суд — все эти бейджи испарятся.

Поэтому я выбрал подход «не форк». Объяснение ниже.

Главная техническая идея: патчим upstream на лету

Обычная схема when ты делаешь продукт на основе open-source:

  1. Форкаешь репо

  2. Меняешь нужное прямо в коде

  3. Поддерживаешь fork вечно: каждое обновление upstream’а вручную мержишь и решаешь конфликты

Это работает, но через год upstream уйдёт далеко, и поддерживать форк становится больно. У RustDesk коммиты прилетают каждый день.

Я делаю иначе. Репозиторий с workflow-сборками просто скачивает upstream и патчит sed-ом в момент сборки клиента:

- name: Checkout RustDesk source (master)  uses: actions/checkout@v4  with:    repository: rustdesk/rustdesk    ref: master    submodules: recursive- name: Aggressive rebrand  if: ${{ inputs.rebrand_strings == 'true' }}  run: |    find ./flutter/lib -name "*.dart" -type f \      -exec sed -i "s|RustDesk|${APP_NAME}|g" {} +    sed -i "s|hbb_common::config::APP_NAME.read().unwrap().clone()|\"${APP_NAME}\".to_string()|g" \      ./src/common.rs    sed -i "s|android:scheme=\"rustdesk\"|android:scheme=\"${BRAND_LOWER}\"|g" \      ./flutter/android/app/src/main/AndroidManifest.xml

Каждая сборка тянет свежий upstream (или конкретный тег, какой клиент попросил), накатывает мои патчи, билдит, удаляет. Результат — .exe.apk.dmg.deb под клиента.

Создание клиента

Создание клиента

Что это даёт

  • Я не обязан публиковать форк — потому что форка нет. Есть downstream-патчи, которые публичные (в моём workflow-репо) и AGPL-совместимые.

  • Upstream автоматически обновляется — захотел собрать с RustDesk 1.4.5? Указал version: 1.4.5 — workflow сам подтянет.

  • Каждый клиент компании А — это независимая сборка со своим брендом, своим зашитым tenant slug, своим набором фичей.

Какие проблемы

Главная — upstream ломает имена. Я однажды добавил sed-патч под анкер PopupMenuButton<String>, а upstream переименовал виджет в следующем релизе. Sed молча не нашёл паттерн (continue-on-error: true), сборка прошла, но функционал не добавился. Открыл собранный клиент — нет кнопки «Запросить помощь». Долго искал почему.

Решение: писать defensive multi-anchor patches и проверять что патч реально применился:

sed -i "s|buildTip(context),|buildTip(context),\n  Padding(...)|" desktop_home_page.dartecho "Verification:"grep -c "showSupportRequestDialog" desktop_home_page.dart || \  echo "⚠️ Patch did not apply!"

Сейчас все патчи валидируются — если grep вернул 0, в логах сборки видно проблему, и можно быстро поправить регексп.

Защита от мошенничества: убираем приём входящих с Android

Это та фишка, ради которой Google Play, RuStore и сертификаторы безопасности должны полюбить нашу сборку.

Часть окна сборки клиента Android

Часть окна сборки клиента Android

Сценарий российского мошенничества:

  1. Бабушке звонят «из банка»

  2. Скачайте приложение / AnyDesk / RustDesk

  3. Бабушка диктует свой ID

  4. Мошенник заходит, переводит деньги через Сбербанк Онлайн

Я физически вырезаю входящий режим из Android-сборки. При сборке с флагом outgoing_only=true:

# 1. На уровне Rust: is_outgoing_only() всегда truesed -i -E 's|SyncReturn\(config::is_outgoing_only\(\)\)|SyncReturn(true)|g' \  src/flutter_ffi.rs# 2. На уровне Dart: насильно скрываем Server tabsed -i 's|if (isAndroid && !bind.isOutgoingOnly())|if (false)|' \  flutter/lib/mobile/pages/home_page.dart# 3. Из AndroidManifest удаляем опасные permissionssed -i '/<uses-permission[^>]*FOREGROUND_SERVICE_MEDIA_PROJECTION[^>]*/d' "$MANIFEST"sed -i '/<uses-permission[^>]*RECORD_AUDIO[^>]*/d' "$MANIFEST"sed -i '/<uses-permission[^>]*SYSTEM_ALERT_WINDOW[^>]*/d' "$MANIFEST"

В результирующем APK физически нет ни UI для принятия подключения, ни разрешений на захват экрана/звука/overlay. Скачал, поставил, открыл — единственное что можно делать: набрать ID и подключиться к ПК. Принять подключение невозможно. От слова совсем.

Это:

  • Защищает бабушек 😉

  • Позволяет публиковаться в Google Play / RuStore без блокировок (теоретически — Kaspersky всё ещё ругается, см. ниже)

  • Подходит к подаче в Реестр российского ПО как «безопасный инструмент удалённого администрирования»

А что с Kaspersky?

Когда я подал APK в RuStore, Касперский задетектил его как not-a-virus:HEUR:RemoteAdmin.AndroidOS.RustDesk.a. Префикс not-a-virus означает PUA (Potentially Unwanted Application) — тот же класс что и AnyDesk/TeamViewer получают.

Эвристика срабатывает на сигнатуру librustdesk.so в APK. Решение — переименовать нативную библиотеку на лету:

NATIVE_NAME="evertydesk"# Файл .so копируется в jniLibs под новым именемcp ./target/release/liblibrustdesk.so \   ./flutter/android/app/src/main/jniLibs/arm64-v8a/lib${NATIVE_NAME}.so# И Kotlin загружает по новому имениsed -i "s|System.loadLibrary(\"rustdesk\")|System.loadLibrary(\"${NATIVE_NAME}\")|" \  ./flutter/android/app/src/main/kotlin/com/.../ffi.kt

Плюс апелляция в RuStore с объяснением что это PUA, что наш клиент outgoing-only, и что вообще «not-a-virus» — это не вирус. Должно решить. Ждемс.

Smart Agent — peer-to-peer помощь между сотрудниками

Эта фича — то, чего нет ни у TeamViewer, ни у AnyDesk, ни у самого RustDesk.

Окно собранного клиента

Окно собранного клиента

В кастомный клиент я инжектирую отдельный Dart-сервис (agent_service.dart), который работает фоном внутри RustDesk-процесса. Делает три вещи:

  1. Heartbeat на наш сервер каждую минуту (онлайн-статус машины)

  2. Inbox-полл каждые 30 секунд (входящие уведомления — например push от админа)

  3. Запрос помощи — пользователь жмёт кнопку в UI клиента, выбирает конкретного оператора, тот получает popup с кнопками [Принять] [Через 10 мин] [Через час] [Отклонить]

Архитектурно это надстройка над RustDesk-протоколом, отдельный канал, не использующий relay. Через наш HTTP API.

class AgentService {  static const Duration kInboxInterval = Duration(seconds: 30);  static const Duration kHeartbeatInterval = Duration(minutes: 1);  Future<void> _checkInbox() async {    final resp = await http.get(      Uri.parse('$_apiServer/admin/agent/inbox').replace(queryParameters: {        'machine_id': _machineId,        'service_key': _serviceKey,      }),    ).timeout(kHttpTimeout);    if (resp.statusCode != 200) {      _inboxFailures++;      return;    }    _inboxFailures = 0;    final items = jsonDecode(resp.body)['items'] as List;    for (final item in items) {      if (item['type'] == 'support_ping') {        showSupportPingDialog(ctx, item); // Popup сдействиями      }      // ... друге типы: banner, poll, config_update    }  }}

Здесь была интересная боль: первый раз я просто хранил target_machine_id как строку в поле target_ids AgentNotification. А серверный inbox-фильтр парсит это как JSON-массив. Json.Unmarshal падал → continue → уведомление молча не доставлялось. Симптом — «нажимаю Запросить помощь, ничего не происходит». Диагностика заняла пару часов:

// Было:TargetIds: resolvedTarget,  // ← "abc123"// Стало:targetIdsJson, _ := json.Marshal([]string{resolvedTarget})TargetIds: string(targetIdsJson),  // ← `["abc123"]`

Урок: если ваш парсер строгий, и у вас есть continue-on-error, ошибки прячутся. Поэтому я везде, где раньше было silent-fail, теперь добавляю counter (_inboxFailures++) и логирую.

Уровень 2 — устройство привязывается к тенанту при подключении. Это та часть, где RustDesk родной из коробки ничего не знает про тенанта. У меня смешно: heartbeat от RustDesk-клиента (/api/heartbeat) не несёт никакого tenant-id’а — там только {id, uuid, conns}. То есть все 10 компаний шлют heartbeat в один и тот же default-аккаунт.

Решение нашлось через Smart Agent. Он-то знает свой service_key (slug компании, зашитый при сборке) — каждые 60 секунд шлёт его на /admin/agent/heartbeat. Сервер:

//        на каждом агентском heartbeat:if saId > 0 {  // Находим Device с тем же rustdesk_id и перепривязывем к правильному тенанту  c.Db.Where("rustdesk_id = ?", machineId).    Cols("service_account_id", "is_pending").    Update(&model.Device{      ServiceAccountId: saId,      IsPending:        false,    })}

То есть устройство сначала попадает в «pending pool» (не видно никому), а потом Smart Agent его «забирает» в нужный тенант. Через 30-60 секунд после установки клиента машина появляется в кабинете нужной компании.

Дополнительно — pending-pool работает как «модерация»: если по какой-то причине агент не запустился, владелец видит в админке кнопку «Принять устройство» и может вручную привязать к клиенту. Это AnyDesk-style enrollment на минималках.

Корпоративный SSO: Active Directory + Яндекс ID

Часть страницы для настройки AD

Часть страницы для настройки AD

LDAP/AD реализован так, что не требует патча клиента. Юзер в RustDesk жмёт «Sign in», вводит доменный логин/пароль. Сервер видит в его tenant’е есть LDAP — пробует bind. Если успех -создаёт User, возвращает токен. Klein client doesn’t know it talked to LDAP — просто принял.

// В service/ldap.go:func LdapAuthenticate(cfg *model.LdapConfig, username, password string) (*LdapAuthResult, error) {    conn, _ := ldap.DialURL(cfg.ServerUrl)    defer conn.Close()    // 1 Bind как сервисный  аккаунт для поиска    conn.Bind(cfg.BindDn, cfg.BindPassword)    // 2 Найти пользователя по фильтру    filter := strings.ReplaceAll(cfg.UserFilter, "{username}",                                 ldap.EscapeFilter(username))    res, _ := conn.Search(...)    // 3 Bind КАК этот пользователь это и есть проверка пароля    if err := conn.Bind(res.Entries[0].DN, password); err != nil {        return nil, fmt.Errorf("invalid_credentials")    }    // 4 Опционально проверить вхождение в разрешёную группу    return &LdapAuthResult{...}, nil}

И в стандартном /api/login:

if !get {    // Пользователь не существует локально    res, accId, err := LdapTryAllAccounts(db, username, password)    if err == nil && res != nil {        // LDAP принял! Создаём User под нужным тенантом        user = autoProvisionFromLdap(res, accId)    }}

Результат: компания-клиент включает LDAP в кабинете, все её сотрудники могут логиниться в RustDesk-клиент доменными учётками без отдельной регистрации. Когда сотрудника увольняют (отключают в AD), его следующий вход в RustDesk просто не пройдёт.

Яндекс ID реализован через стандартный OAuth 2.0 + Device Authorization Grant (RFC 8628). Для веб-кабинета — кнопка «Войти через Яндекс», редирект, callback. Для desktop-клиента — Device Code flow: клиент получает code, показывает диалог «Откройте yandex.com/device, введите ABCD-EFGH», пользователь вводит, клиент получает токен. Это стандарт для CLI-инструментов (gh auth loginaws sso login).

Биллинг и оплата для российских юрлиц

Это та область, где западные SaaS-стартапы экономят время используя Stripe. В РФ Stripe не работает.

У меня:

  • YooKassa для физлиц — встраивается напрямую в кабинет через их API

  • Оплата по счёту юрлицам — отдельный flow:

    1. Клиент в кабинете жмёт «Оплатить по счёту», вводит ИНН/КПП/реквизиты

    2. Сервер создаёт InvoiceRequest с уникальным номером 2026-0042

    3. Я в админке вижу заявку, выписываю PDF в банке (у меня Точка), загружаю в систему

    4. Клиент получает email со ссылкой на скачивание счёта

    5. Платит → деньги падают на расчётный счёт

    6. Я отмечаю в админке «Оплачен» → подписка активируется автоматом

Не сказать что это бизнес-инновация, но никакого готового решения для embedded B2B-биллинга в РФ просто нет. У всех костыли.

Стек, цифры, обещания

Компонент

Технология

Backend API

Go + Iris MVC

База

PostgreSQL 16

Frontend (кабинет, landing)

Vue 3 + Naive UI + Vite

Клиент (десктоп)

RustDesk official + patches

Smart Agent (внутри клиента)

Flutter/Dart

Сборка

GitHub Actions

Деплой

Docker Compose

Сборка одного клиента — 25-40 минут (большая часть — Rust компилируется). Параллельно собирается под все 4 платформы (Win/Linux/macOS/Android).

К концу мая 2026 года планирую:

  • Стабильный SaaS-релиз

  • Андроид в Google Play + RuStore (RuStore сейчас на апелляции из-за HEUR:RustDesk детекта)

Тарифы весят тестовые,

В отличие от RuDesktop, я не строю компанию с инвесторами, бизнес-планом на круги Эйлера, и сертификацией ФСТЭК в первом квартале как KPI.

Я просто решаю проблему которая меня саму годами бесила: «дайте мне нормальный российский remote desktop с честной лицензионной чистотой, безопасный для конечных пользователей, и оплачиваемый по российским способам». К тому же, признаюсь я всегда жадно читал статьи про RustDesk на хабре, чтож, я набрал уже 4 «клиента — тестировщика» так сказать, будем развивать дальше.

Ну и самое важное дума, Self-hosted версия: для тех кому облако не подходит

Не каждой компании можно загнать удалённую поддержку в чужое облако. Госы, банки, медицинские учреждения, ВПК-сегмент, корпорации с собственной службой ИБ — у них в политиках чёрным по белому: данные обработки внутри периметра, точка. С этим бесполезно спорить.

Поэтому параллельно с SaaS я делаю self-hosted версию того же продукта. Та же кодовая база, тот же функционал, но всё запускается у клиента на его железе или в его частном облаке.

Что входит

# docker-compose.yml у клиента после установкиservices:  everty-desk-api:        # Go backend + Vue cabinet    image: everty-desk/api:1.0  everty-desk-postgres:   # PostgreSQL    image: postgres:16-alpine  everty-desk-hbbs:       # RustDesk ID server    image: rustdesk/rustdesk-server:latest  everty-desk-hbbr:       # RustDesk relay    image: rustdesk/rustdesk-server:latest

Один docker compose up -d, скрипт-установщик pro сертификаты Let’s Encrypt (или загрузка своих), миграции БД — всё. Никаких внешних звонков на наши серверы. Полная изоляция.

Как лицензируется

Лицензия — годовая, активируется офлайн через подписанный JWT-ключ. Никакого «онлайн-чекина», который вырубит сервер если интернет лёг. Есть пока простая документация тут.

Лицензия привязана к доменам (whitelisted hostnames в самом JWT), что предотвращает копирование на другой инстанс. При истечении — graceful degradation:за месяц до конца показываются предупреждения, после истечения новые подключения блокируются, но уже существующие машины и адресная книга сохраняются до момента продления. Без «всё пропало».

В self-hosted входит буквально всё, только все настройки, авторизации, токены все на вас.

Статус: бета, релиз — конец июня

Прямо сейчас self-hosted версия в тестировании — у меня запущена параллельная инсталляция на отдельной машине, проверяю миграции, лицензии, изоляцию, обновления. С тестировщиками конечно беда, если кому интересно пишите с пометкой на info@everty.ru.

К концу июня 2026 — стабильный релиз 1.0 с:

  • Полным installer-скриптом (curl install.everty.ru/selfhost.sh | sh)

  • Подробной документацией (от docker compose до nginx reverse-proxy и Let’s Encrypt)

  • Lifecycle-менеджментом лицензий через мою административную панель

  • SLA на исправление багов в первый год

  • Каналом обновлений: stable / beta / edge

Спасибо тем кто дочитал до конца, если хочется обсудить технические детали реализации — буду рад ответить в комментариях.

info@everty.ru
desk.everty.ru

ссылка на оригинал статьи https://habr.com/ru/articles/1038580/