Compare commits
11 Commits
master
...
vps-rustde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
038b9791c5 | ||
|
|
43e7b30f41 | ||
|
|
32451eb0f8 | ||
|
|
3f49556cd7 | ||
|
|
554aa6008d | ||
|
|
be123e8057 | ||
|
|
a6492fc1b9 | ||
|
|
b93147dd67 | ||
|
|
90fa901119 | ||
|
|
00c36b8d7e | ||
|
|
a0b2b5ebf7 |
430
README.md
430
README.md
@@ -1,251 +1,300 @@
|
|||||||
# RDtop — RustDesk Headless Display Setup
|
# RDtop — RustDesk Headless Display Setup
|
||||||
|
|
||||||
**Автоматический виртуальный дисплей для RustDesk на Intel iGPU без подключённого HDMI монитора.**
|
**Автоматический виртуальный дисплей для RustDesk без подключенного HDMI монитора. Работает на любой Linux машине (Intel iGPU, AMD, NVIDIA, CPU-only VPS).**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Содержание
|
## Содержание
|
||||||
|
|
||||||
1. [Кратко о проблеме](#кратко-о-проблеме)
|
1. [Проблема](#проблема)
|
||||||
2. [Архитектура решения](#архитектура-решения)
|
2. [Решение](#решение)
|
||||||
3. [Что где лежит и куда ставить](#что-где-лежит-и-куда-ставить)
|
3. [Файлы проекта](#файлы-проекта)
|
||||||
4. [Установка](#установка)
|
4. [Установка](#установка)
|
||||||
5. [Как это работает на самом деле](#как-это-работает-на-самом-деле)
|
5. [Как это работает](#как-это-работает)
|
||||||
6. [Проверка](#проверка)
|
6. [Проверка](#проверка)
|
||||||
7. [Troubleshooting](#troubleshooting)
|
7. [Troubleshooting](#troubleshooting)
|
||||||
8. [Откат](#откат)
|
8. [Откат](#откат)
|
||||||
9. [Технические детали](#технические-детали)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Кратко о проблеме
|
## Проблема
|
||||||
|
|
||||||
Оборудование: **Intel Alder Lake-N (i915)**, Ubuntu 24.04 (Noble), X11 (GDM/GNOME).
|
Оборудование: **Intel Alder Lake-N (i915)**, Ubuntu 24.04, X11 (GDM/GNOME).
|
||||||
|
|
||||||
### Что происходит
|
### Что происходит
|
||||||
|
|
||||||
1. При подключённом HDMI — Xorg инициализирует `HDMI-1` на отдельный CRTC.
|
1. При подключенном HDMI — Xorg инициализирует `HDMI-1` на отдельный CRTC.
|
||||||
2. Когда кабель отключают — **CRTC уничтожается**.
|
2. Когда кабель отключают — **CRTC уничтожается**.
|
||||||
3. У Xorg не остаётся активного выхода. Может остаться чёрный экран или fallback resolution 8×8.
|
3. У Xorg не остается активного выхода. Может остаться черный экран или fallback 8×8.
|
||||||
4. RustDesk всё ещё подключён, но захватывает `Screen 0` — и видит либо чёрный экран, либо 8×8 пикселей.
|
4. RustDesk все еще подключен, но захватывает `Screen 0` — и видит либо черный экран, либо 8×8 пикселей.
|
||||||
|
|
||||||
### Почему стандартные способы не помогли
|
### Почему стандартные способы не помогли
|
||||||
|
|
||||||
| Способ | Результат |
|
| Способ | Результат |
|
||||||
|--------|-----------|
|
|--------|-----------|
|
||||||
| `Option "VirtualHeads" "1"` (intel driver) | **Не работает** на Alder Lake-N: VIRTUAL1 создаётся, но intel driver не инициализирует на Gen11+. |
|
| `Option "VirtualHeads" "1"` (intel driver) | **Работает только с кабелем на момент загрузки.** Без HDMI — VIRTUAL1 не создается. |
|
||||||
| modesetting driver | Даёт VIRTUAL1 через randr, но через uAPI DRI он остаётся disconnected. |
|
| modesetting driver | Создает VIRTUAL1 через randr, но после reboot пропадает. |
|
||||||
| `NoOutputInitialSize` | Требует пересборки xorg или патчей. |
|
| `NoOutputInitialSize` | Требует пересборки xorg или патчей. |
|
||||||
| Dummy plug (аппаратный) | Работает, но нужно покупать. Для программного режима — нет простого способа. |
|
| Dummy plug (аппаратный) | Работает, но нужно покупать. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Архитектура решения
|
## Решение
|
||||||
|
|
||||||
|
### Хост: Dummy driver + kernel EDID fallback
|
||||||
|
|
||||||
|
Работает **без кабеля HDMI на момент загрузки** — dummy driver не зависит от физических выходов.
|
||||||
|
|
||||||
```
|
```
|
||||||
+-----------------------------------------------------------------------------+
|
+------------------------------------------------------------------------+
|
||||||
| X11 Screen 0: 1920x1080 |
|
| Xorg Dummy Driver (1920×1080) |
|
||||||
| |
|
| +-------------------------+ |
|
||||||
| +------------------------+ +------------------------------------+ |
|
| | DUMMY0 = primary | ← RustDesk --server захватывает |
|
||||||
| | VIRTUAL1 | | HDMI1 | |
|
| | 1920×1080 @ 60Hz | |
|
||||||
| | PRIMARY | | CLONE (--same-as VIRTUAL1) | |
|
| +-------------------------+ |
|
||||||
| | 1920×1080 @ 60Hz | | 1920×1080 @ 60Hz | |
|
| |
|
||||||
| | Full HD | | Full HD (если кабель подключён) | |
|
| При подключении HDMI кабеля: |
|
||||||
| | ВСЕГДА АКТИВЕН | | При отключении → off | |
|
| скрипт hdmi-fallback.sh клонирует HDMI1 на DUMMY0 |
|
||||||
| +------------------------+ +------------------------------------+ |
|
| При отключении: HDMI1 off, DUMMY0 остается primary |
|
||||||
| |
|
+------------------------------------------------------------------------+
|
||||||
| RustDesk: всегда захватывает PRIMARY (VIRTUAL1), вне зависимости |
|
|
||||||
| от того, подключён HDMI или нет. |
|
|
||||||
+-----------------------------------------------------------------------------+
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ключевой принцип:** `VIRTUAL1 = primary`. HDMI1 — это просто клон. Когда HDMI1 отключается, его CRTC удаляется, но primary остаётся — RustDesk всегда видит рабочий стол.
|
### Kernel EDID fallback (дополнительно)
|
||||||
|
|
||||||
|
Параметры ядра:
|
||||||
|
```
|
||||||
|
video=HDMI-A-1:1920x1080@60 drm.edid_firmware=HDMI-A-1:edid/samsung.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Это заставляет ядро `i915` инициализировать HDMI-A-1 с сохраненным EDID даже без кабеля. Если dummy driver не сработает (редкий случай), ядро все равно создаст виртуальный framebuffer.
|
||||||
|
|
||||||
|
### VPS (CPU-only): Dummy driver + Xorg + RustDesk
|
||||||
|
|
||||||
|
Тот же dummy driver, но без GPU. Работает на любом VPS.
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------------------------------------------------------+
|
||||||
|
| Xorg Dummy Driver (1920×1080) |
|
||||||
|
| +-------------------------+ |
|
||||||
|
| | DUMMY0 = primary | ← RustDesk --server захватывает |
|
||||||
|
| | 1920×1080 @ 60Hz | |
|
||||||
|
| +-------------------------+ |
|
||||||
|
| Нет GPU, нет DRM — работает на любом VPS |
|
||||||
|
+------------------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Что где лежит и куда ставить
|
## Файлы проекта
|
||||||
|
|
||||||
| Файл репозитория | Назначение | Системный путь |
|
| Файл репозитория | Назначение | Системный путь |
|
||||||
|------------------|------------|----------------|
|
|------------------|------------|----------------|
|
||||||
| `bin/hdmi-fallback.sh` | Мониторинг `HDMI-A-1` в sysfs и автоматическое переключение на fallback | `~/.local/bin/hdmi-fallback.sh` |
|
| `bin/hdmi-fallback.sh` | Мониторинг HDMI-A-1 и авто-fallback на DUMMY0/VIRTUAL1 | `~/.local/bin/hdmi-fallback.sh` |
|
||||||
| `config/x11/90-fallback.conf` | Xorg конфиг: гарантирует framebuffer 1920×1080 даже при отключённом HDMI | `/etc/X11/xorg.conf.d/90-fallback.conf` |
|
| `bin/vps-start-xorg.sh` | Стартер Xorg с dummy driver на VPS | `/usr/local/bin/start-xorg-dummy.sh` |
|
||||||
| `config/systemd/hdmi-fallback.service` | systemd user service: запускает скрипт после входа в графическую сессию | `~/.config/systemd/user/hdmi-fallback.service` |
|
| `config/x11-host/20-dummy-headless.conf` | Dummy driver headless config | `/etc/X11/xorg.conf.d/20-dummy-headless.conf` |
|
||||||
| `install.sh` | Установщик: делает всё автоматически | (запускается один раз) |
|
| `config/x11-host/20-intel-virtual.conf.bak` | Backup старого Intel config (не используется) | — |
|
||||||
|
| `config/systemd/hdmi-fallback.service` | systemd user unit (host) | `~/.config/systemd/user/hdmi-fallback.service` |
|
||||||
|
| `config/vps/xorg-dummy.service` | systemd system unit (VPS Xorg) | `/etc/systemd/system/xorg-dummy.service` |
|
||||||
|
| `config/vps/rustdesk-dummy.service` | systemd system unit (VPS RustDesk) | `/etc/systemd/system/rustdesk-dummy.service` |
|
||||||
|
| `install.sh` | Установщик для хоста | (запускается один раз) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
### Способ 1: Одной командой
|
### Хост (Desktop с GPU)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash -c "$(curl -fsSL https://git.softuniq.eu/NW/RDtop/raw/branch/main/install.sh)"
|
bash -c "$(curl -fsSL https://git.softuniq.eu/NW/RDtop/raw/branch/main/install.sh)"
|
||||||
```
|
```
|
||||||
|
|
||||||
После установки **перезагрузить компьютер** или перезапустить GDM:
|
После установки **перезагрузить компьютер**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl restart gdm
|
sudo reboot
|
||||||
```
|
```
|
||||||
|
|
||||||
### Способ 2: Вручную
|
**Вручную:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Клонируем репозиторий
|
# 1. Клонируем
|
||||||
git clone https://git.softuniq.eu/NW/RDtop.git ~/RDtop
|
git clone https://git.softuniq.eu/NW/RDtop.git ~/RDtop && cd ~/RDtop
|
||||||
cd ~/RDtop
|
|
||||||
|
|
||||||
# 2. Устанавливаем dummy драйвер (опционально, запасной)
|
# 2. Dummy driver config (главный способ)
|
||||||
sudo apt-get update
|
sudo cp config/x11-host/20-dummy-headless.conf /etc/X11/xorg.conf.d/
|
||||||
sudo apt-get install -y xserver-xorg-video-dummy
|
|
||||||
|
|
||||||
# 3. Копируем X11 конфиг
|
# 3. Удалить старые конфиги если были
|
||||||
sudo mkdir -p /etc/X11/xorg.conf.d
|
sudo rm -f /etc/X11/xorg.conf.d/20-intel-virtual.conf
|
||||||
sudo cp config/x11/90-fallback.conf /etc/X11/xorg.conf.d/
|
sudo rm -f /etc/X11/xorg.conf.d/90-fallback.conf
|
||||||
|
|
||||||
# 4. Устанавливаем скрипт
|
# 4. Сохранить EDID для kernel fallback (опционально)
|
||||||
mkdir -p ~/.local/bin
|
sudo mkdir -p /lib/firmware/edid
|
||||||
|
sudo cp /sys/class/drm/card0-HDMI-A-1/edid /lib/firmware/edid/samsung.bin
|
||||||
|
|
||||||
|
# 5. Обновить GRUB для kernel EDID fallback
|
||||||
|
sudo sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=".*"|GRUB_CMDLINE_LINUX_DEFAULT="quiet splash video=HDMI-A-1:1920x1080@60 drm.edid_firmware=HDMI-A-1:edid/samsung.bin"|' /etc/default/grub
|
||||||
|
sudo update-grub
|
||||||
|
|
||||||
|
# 6. Скрипт мониторинга
|
||||||
chmod +x bin/hdmi-fallback.sh
|
chmod +x bin/hdmi-fallback.sh
|
||||||
cp bin/hdmi-fallback.sh ~/.local/bin/
|
cp bin/hdmi-fallback.sh ~/.local/bin/
|
||||||
|
|
||||||
# 5. Устанавливаем systemd unit
|
# 7. systemd unit
|
||||||
mkdir -p ~/.config/systemd/user
|
mkdir -p ~/.config/systemd/user
|
||||||
cp config/systemd/hdmi-fallback.service ~/.config/systemd/user/
|
cp config/systemd/hdmi-fallback.service ~/.config/systemd/user/
|
||||||
|
|
||||||
# 6. Активируем сервис
|
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable --now hdmi-fallback.service
|
systemctl --user enable --now hdmi-fallback.service
|
||||||
|
|
||||||
# 7. Перезагрузка (обязательно для X11)
|
# 8. Перезагрузка
|
||||||
sudo systemctl restart gdm
|
sudo reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант Б: CPU-only VPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Установить Xorg + dummy
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y xserver-xorg xserver-xorg-video-dummy rustdesk
|
||||||
|
|
||||||
|
# 2. Копировать конфиги
|
||||||
|
git clone https://git.softuniq.eu/NW/RDtop.git /tmp/rdtop
|
||||||
|
cd /tmp/rdtop
|
||||||
|
sudo cp config/vps/20-dummy-headless.conf /etc/X11/xorg.conf.d/
|
||||||
|
sudo cp config/vps/xorg-dummy.service /etc/systemd/system/
|
||||||
|
sudo cp config/vps/rustdesk-dummy.service /etc/systemd/system/
|
||||||
|
sudo cp bin/vps-start-xorg.sh /usr/local/bin/start-xorg-dummy.sh
|
||||||
|
chmod +x /usr/local/bin/start-xorg-dummy.sh
|
||||||
|
|
||||||
|
# 3. Остановить конфликтующий gdm
|
||||||
|
systemctl stop gdm.service 2>/dev/null || true
|
||||||
|
systemctl disable gdm.service 2>/dev/null || true
|
||||||
|
|
||||||
|
# 4. Запустить
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now xorg-dummy.service
|
||||||
|
systemctl enable --now rustdesk-dummy.service
|
||||||
|
|
||||||
|
# 5. Проверить
|
||||||
|
export DISPLAY=:0
|
||||||
|
xrandr --listmonitors
|
||||||
|
# Должно показать: DUMMY0 primary 1920x1080
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Как это работает на самом деле
|
## Как это работает
|
||||||
|
|
||||||
### Шаг 1. Xorg fallback конфиг (`90-fallback.conf`)
|
### Хост (Intel iGPU)
|
||||||
|
|
||||||
|
1. **Dummy driver** (`Driver "dummy"`) создает DUMMY0 сразу при старте Xorg, **независимо от HDMI**.
|
||||||
|
2. **Kernel EDID fallback** (`drm.edid_firmware`) — если dummy каким-то образом не сработает, ядро `i915` инициализирует HDMI-A-1 с сохраненным EDID.
|
||||||
|
3. **Скрипт** `hdmi-fallback.sh` при старте:
|
||||||
|
- Всегда активирует DUMMY0 как primary
|
||||||
|
- Если HDMI connected — делает HDMI1 clone (`--same-as`)
|
||||||
|
- Если HDMI disconnected — выключает HDMI1, DUMMY0 остается primary
|
||||||
|
4. **RustDesk** захватывает DUMMY0 (primary).
|
||||||
|
|
||||||
|
### VPS (CPU-only)
|
||||||
|
|
||||||
|
**Архитектура:** Xvfb :99 + xfce4-session + xfwm4 + xfce4-panel + RustDesk client
|
||||||
|
|
||||||
|
1. **Xvfb** — виртуальный framebuffer на `:99` 1920×1080x24, доступен для захвата X11
|
||||||
|
2. **xfce4-session** — полноценный XFCE рабочий стол (окна, панель, иконки)
|
||||||
|
3. **RustDesk** — клиент подключён к публичному relay `rs-ny.rustdesk.com:21116`
|
||||||
|
4. **Вход** — ID из 9 цифр + пароль
|
||||||
|
|
||||||
```
|
```
|
||||||
Section "Screen"
|
+------------------------------------------------------------------------+
|
||||||
Identifier "AutoScreen"
|
| Xvfb :99 (1920x1080x24) |
|
||||||
...
|
| +-------------------------------------------------------+ |
|
||||||
SubSection "Display"
|
| | XFCE Desktop (xfwm4 + xfce4-panel + xfdesktop) | |
|
||||||
Depth 24
|
| | +---------------------------------------------+ | |
|
||||||
Virtual 1920 1080
|
| | | [Панель XFCE] [Иконки] [Контекстное меню]| | |
|
||||||
EndSubSection
|
| | +---------------------------------------------+ | |
|
||||||
EndSection
|
| | ← RustDesk client захватывает экран через X11 | |
|
||||||
|
| +-------------------------------------------------------+ |
|
||||||
|
| SSH / tmux (персистентность) |
|
||||||
|
+------------------------------------------------------------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
Параметр `Virtual 1920 1080` задаёт размер виртуального framebuffer на уровне экрана X11. Даже если физических выхода нет, Xorg создаёт `Screen` фиксированного размера. **Без этого при отключённом HDMI Xorg мог задать fallback resolution 8×8 или не инициализировать экран вовсе.**
|
**Персистентность:** `tmux` (не systemd) — `pkill` в ExecStartPre убивал Xvfb и xfce.
|
||||||
|
|
||||||
### Шаг 2. Modesetting драйвер создаёт VIRTUAL1
|
|
||||||
|
|
||||||
Драйвер `modesetting` (встроен в `xorg-server`) на современных Intel GPU создаёт `VIRTUAL1` как software output. Проверка:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
xrandr | grep VIRTUAL
|
|
||||||
# VIRTUAL1 disconnected (normal left inverted right x axis y axis)
|
|
||||||
# VIRTUAL2 disconnected (normal left inverted right x axis y axis)
|
|
||||||
```
|
|
||||||
|
|
||||||
Это отдаётся через uAPI randr, но не через DRI.
|
|
||||||
|
|
||||||
### Шаг 3. Мы активируем VIRTUAL1
|
|
||||||
|
|
||||||
```bash
|
|
||||||
xrandr --addmode VIRTUAL1 "1920x1080_60.00"
|
|
||||||
xrandr --output VIRTUAL1 --mode "1920x1080_60.00" --primary
|
|
||||||
xrandr --output HDMI1 --auto --same-as VIRTUAL1
|
|
||||||
```
|
|
||||||
|
|
||||||
`--same-as` означает клонирование. Обе точки вывода ссылаются на **один и тот же framebuffer**. Когда HDMI1 теряет кабель, его CRTC удаляется, но данные остаются на VIRTUAL1.
|
|
||||||
|
|
||||||
### Шаг 4. Скрипт `hdmi-fallback.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Цикл каждые 2 секунды проверяет sysfs:
|
|
||||||
# /sys/class/drm/card1-HDMI-A-1/status
|
|
||||||
# connected → HDMI1 = clone(VIRTUAL1)
|
|
||||||
# disconnected → HDMI1 = off, VIRTUAL1 = primary
|
|
||||||
```
|
|
||||||
|
|
||||||
**Почему не через udev?** `drm_connector` в ядре i915 не генерирует reliable hotplug events через sysfs. Поэтому используется polling каждые 2 сек. Это безопасно — sysfs read очень лёгкий.
|
|
||||||
|
|
||||||
### Шаг 5. Dummy driver — запасной вариант
|
|
||||||
|
|
||||||
Если VIRTUAL1 по какой-то причине не создаётся (например, если modesetting не загружается), `dummy_drv.so` позволяет создать виртуальный выход через `xf86-video-dummy`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Установка:
|
|
||||||
sudo apt-get install xserver-xorg-video-dummy
|
|
||||||
|
|
||||||
# Конфигурация:
|
|
||||||
# Driver "dummy", Monitor, Screen → DUMMY-1
|
|
||||||
```
|
|
||||||
|
|
||||||
Однако на Intel i915 Alder Lake-N modesetting + VIRTUAL1 работает стабильнее.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Проверка
|
## Проверка
|
||||||
|
|
||||||
|
### Хост (после reboot без HDMI)
|
||||||
```bash
|
```bash
|
||||||
# 1. После перезагрузки — проверяем primary output
|
# Должно показать DUMMY0 как primary
|
||||||
|
xrandr --listmonitors
|
||||||
|
# Ожидаемый вывод:
|
||||||
|
# 0: +*DUMMY0 1920/508x1080/286+0+0 DUMMY0 [PRIMARY]
|
||||||
|
|
||||||
|
# Если HDMI подключен:
|
||||||
|
# 1: +HDMI-1 1920/508x1080/286+0+0 HDMI-1 [CLONE]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сервис
|
||||||
|
```bash
|
||||||
|
systemctl --user status hdmi-fallback.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Логи
|
||||||
|
```bash
|
||||||
|
cat ~/.local/logs/drm-hotplug.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### VPS
|
||||||
|
```bash
|
||||||
|
export DISPLAY=:0
|
||||||
xrandr --listmonitors
|
xrandr --listmonitors
|
||||||
# Должно быть:
|
# Должно быть:
|
||||||
# 0: +*VIRTUAL1 1920/508x1080/286+0+0 VIRTUAL1 [PRIMARY]
|
# 0: +*DUMMY0 1920x... DUMMY0 [PRIMARY]
|
||||||
# 1: +HDMI1 1920/700x1080/390+0+0 HDMI1 [CLONE]
|
|
||||||
|
|
||||||
# 2. Проверяем сервис
|
# RustDesk
|
||||||
systemctl --user status hdmi-fallback.service
|
ps aux | grep rustdesk
|
||||||
# ● hdmi-fallback.service - HDMI to VIRTUAL1 Fallback Monitor
|
# /usr/lib/rustdesk/rustdesk --server должен быть запущен
|
||||||
# Active: active (running)
|
|
||||||
|
|
||||||
# 3. Смотрим лог
|
|
||||||
journalctl --user -u hdmi-fallback.service -f
|
|
||||||
|
|
||||||
# 4. Тест: вытаскиваем HDMI
|
|
||||||
# Через 2-3 секунды xrandr должен показывать только VIRTUAL1,
|
|
||||||
# при этом RustDesk продолжает работать.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### VIRTUAL1 не появляется в xrandr
|
### DUMMY0 не появляется после reboot
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Добавляем modeline вручную
|
# Проверить dummy driver:
|
||||||
xrandr --addmode VIRTUAL1 "1920x1080_60.00"
|
grep "Loading.*dummy" /var/log/Xorg.0.log
|
||||||
# Если ошибка — создаём моделайн:
|
# Должно быть: (II) Loading /usr/lib/xorg/modules/drivers/dummy_drv.so
|
||||||
xrandr --newmode "1920x1080_60.00" 173.00 1920 2048 2248 2576 1080 1083 1088 1120
|
|
||||||
xrandr --addmode VIRTUAL1 "1920x1080_60.00"
|
# Проверить конфиг:
|
||||||
|
ls /etc/X11/xorg.conf.d/20-dummy-headless.conf
|
||||||
|
# Должен существовать
|
||||||
|
|
||||||
|
# Если intel driver загружается вместо dummy:
|
||||||
|
# Убедиться что нет других конфликтующих xorg.conf:
|
||||||
|
ls /etc/X11/xorg.conf 2>/dev/null || echo "OK: no xorg.conf"
|
||||||
```
|
```
|
||||||
|
|
||||||
### RustDesk всё ещё чёрный экран
|
### RustDesk черный экран
|
||||||
|
|
||||||
Проверка primary:
|
|
||||||
```bash
|
```bash
|
||||||
|
# Проверить primary:
|
||||||
xrandr --verbose | grep primary
|
xrandr --verbose | grep primary
|
||||||
# Должно быть: VIRTUAL1 connected primary 1920x1080+0+0
|
# Должно быть: DUMMY0 connected primary
|
||||||
|
|
||||||
|
# Если primary не DUMMY0:
|
||||||
|
xrandr --output DUMMY0 --mode "1920x1080" --primary
|
||||||
|
xrandr --output HDMI-1 --auto --same-as DUMMY0
|
||||||
```
|
```
|
||||||
|
|
||||||
Если primary = HDMI1 — переключаем:
|
### VPS: RustDesk не видит DUMMY0
|
||||||
```bash
|
|
||||||
xrandr --output VIRTUAL1 --mode "1920x1080_60.00" --primary
|
|
||||||
xrandr --output HDMI1 --auto --same-as VIRTUAL1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Сервис не запускается
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Проверь лог скрипта
|
# Проверить что Xorg запущен:
|
||||||
cat ~/.local/logs/drm-hotplug.log
|
ls /tmp/.X11-unix/X0
|
||||||
|
# Должен быть сокет
|
||||||
|
|
||||||
# Проверь конфиг юнита
|
# Если нет — запустить вручную:
|
||||||
systemctl --user cat hdmi-fallback.service
|
/usr/local/bin/start-xorg-dummy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -253,86 +302,51 @@ systemctl --user cat hdmi-fallback.service
|
|||||||
## Откат
|
## Откат
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Удалить конфиги X11
|
# Хост
|
||||||
|
sudo rm -f /etc/X11/xorg.conf.d/20-dummy-headless.conf
|
||||||
|
sudo rm -f /etc/X11/xorg.conf.d/20-intel-virtual.conf
|
||||||
sudo rm -f /etc/X11/xorg.conf.d/90-fallback.conf
|
sudo rm -f /etc/X11/xorg.conf.d/90-fallback.conf
|
||||||
|
|
||||||
# 2. Остановить и удалить сервис
|
|
||||||
systemctl --user disable --now hdmi-fallback.service
|
systemctl --user disable --now hdmi-fallback.service
|
||||||
rm -f ~/.config/systemd/user/hdmi-fallback.service
|
|
||||||
|
|
||||||
# 3. Удалить скрипт
|
|
||||||
rm -f ~/.local/bin/hdmi-fallback.sh
|
rm -f ~/.local/bin/hdmi-fallback.sh
|
||||||
|
|
||||||
# 4. Перезагрузка
|
# Вернуть стандартный GRUB:
|
||||||
sudo reboot
|
sudo sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=".*"|GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"|' /etc/default/grub
|
||||||
|
sudo update-grub
|
||||||
|
|
||||||
|
# VPS
|
||||||
|
systemctl disable --now xorg-dummy.service
|
||||||
|
systemctl disable --now rustdesk-dummy.service
|
||||||
|
sudo rm -f /etc/systemd/system/xorg-dummy.service
|
||||||
|
sudo rm -f /etc/systemd/system/rustdesk-dummy.service
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Технические детали
|
## Технические детали
|
||||||
|
|
||||||
### Железо
|
### Почему dummy driver, а не intel с VirtualHeads?
|
||||||
|
|
||||||
```
|
`intel` driver + `VirtualHeads=1` работает **только если HDMI подключен при загрузке**. Без кабеля — ядро не создает connector/CRTC для HDMI-A-1, и `VirtualHeads` не помогает. Dummy driver не зависит от физических выходов.
|
||||||
lspci | grep VGA
|
|
||||||
# 00:02.0 VGA compatible controller: Intel Corporation Alder Lake-N [UHD Graphics]
|
|
||||||
|
|
||||||
# Ядро: i915
|
### Почему kernel EDID fallback?
|
||||||
cat /sys/class/drm/card1-HDMI-A-1/status
|
|
||||||
# connected (или disconnected)
|
|
||||||
|
|
||||||
# Только 1 CRTC:
|
Параметры `video=HDMI-A-1:1920x1080@60` и `drm.edid_firmware=HDMI-A-1:edid/samsung.bin` заставляют ядро `i915` инициализировать connector с фиктивным EDID. Это "страховка": если dummy driver по какой-то причине не загрузится (например, обновление Xorg удалит его), ядро все равно создаст выход.
|
||||||
cat /sys/kernel/debug/dri/*/i915_display_info | grep "CRTC"
|
|
||||||
# [CRTC:80:pipe A] — один pipe
|
|
||||||
```
|
|
||||||
|
|
||||||
### Почему не intel драйвер?
|
### Почему клон, а не extended desktop?
|
||||||
|
|
||||||
Драйвер `xf86-video-intel` — legacy. Официально **не рекомендуется** для Gen 9 и новее (Broadwell+). На Alder Lake-N он вообще не инициализируется корректно. Ubuntu по умолчанию использует `modesetting` — и это правильно.
|
`--same-as` клонирует framebuffer. При disconnect HDMI clone просто перестает рендериться, но primary (DUMMY0) не затрагивается — Xorg не теряет framebuffer.
|
||||||
|
|
||||||
### Почему именно клон, а не side-by-side?
|
### Dummy driver на VPS
|
||||||
|
|
||||||
```
|
Не требует GPU, не использует DRM. Работает на любом Linux с xorg-server. RustDesk использует software capture через X11 API.
|
||||||
HDMI1 right-of VIRTUAL1 → CRTC для HDMI1 используется отдельно
|
|
||||||
При отключении HDMI1 → CRTC освобождается
|
|
||||||
→ если VIRTUAL1 не primary, Screen может схлопнуться
|
|
||||||
|
|
||||||
HDMI1 same-as VIRTUAL1 → Один framebuffer, два выхода
|
|
||||||
При отключении HDMI1 → Просто один output становится неактивным
|
|
||||||
→ Primary (VIRTUAL1) никогда не пострадает
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dummy драйвер как fallback
|
|
||||||
|
|
||||||
На некоторых системах modesetting может не создать VIRTUAL1 (например, если DRI отключен). `dummy_drv.so` решает это:
|
|
||||||
|
|
||||||
```
|
|
||||||
Driver "dummy"
|
|
||||||
→ Создаёт DUMMY-1 с фиксированным разрешением
|
|
||||||
→ Всегда работает, независимо от GPU
|
|
||||||
→ Пакет: xserver-xorg-video-dummy
|
|
||||||
```
|
|
||||||
|
|
||||||
В нашем случае i915 Alder Lake-N — modesetting даёт VIRTUAL1, dummy не нужен, но мы его установили как запасной вариант.
|
|
||||||
|
|
||||||
### Права доступа
|
|
||||||
|
|
||||||
```
|
|
||||||
/etc/X11/xorg.conf.d/90-fallback.conf — root:root 644
|
|
||||||
~/.local/bin/hdmi-fallback.sh — swp:swp 755
|
|
||||||
~/.config/systemd/user/... — swp:swp 644
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Авторы и цели
|
## Авторы
|
||||||
|
|
||||||
- **Задача:** Запуск RustDesk на headless машине (нет физического монитора) с Intel iGPU.
|
- Deploy via AI Agent (Kilo Code Orchestrator)
|
||||||
- **Ключевой инсайт:** `VIRTUAL1` создаётся modesetting driver'ом на Intel Alder Lake-N; нужно только сделать его primary и отслеживать HDMI.
|
- Target hardware: Intel Alder Lake-N (i915), Ubuntu 24.04
|
||||||
- **Результат:** При отключённом HDMI кабеле RustDesk продолжает работать через `VIRTUAL1` (1920×1080, fullscreen).
|
- VPS: CPU-only, Ubuntu 24.04, Hetzner/DigitalOcean/Linode
|
||||||
- **Создано:** через AI agent pipeline (Kilo Code Orchestrator) для SWP.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ссылки
|
## Ссылки
|
||||||
|
|
||||||
|
|||||||
31
bin/vps-start-xorg.sh
Executable file
31
bin/vps-start-xorg.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Start Xorg with dummy driver for headless VPS
|
||||||
|
export DISPLAY=:0
|
||||||
|
export XAUTHORITY=${XAUTHORITY:-/run/user/0/.Xauthority}
|
||||||
|
|
||||||
|
# Generate MIT magic cookie if needed
|
||||||
|
if [ ! -s "$XAUTHORITY" ]; then
|
||||||
|
mkdir -p "$(dirname "$XAUTHORITY")"
|
||||||
|
touch "$XAUTHORITY"
|
||||||
|
xauth -f "$XAUTHORITY" generate :0 MIT-MAGIC-COOKIE-1 2>/dev/null || \
|
||||||
|
xauth -f "$XAUTHORITY" add :0 MIT-MAGIC-COOKIE-1 "$(xxd -l 16 -p /dev/urandom)" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure socket directory
|
||||||
|
mkdir -p /tmp/.X11-unix
|
||||||
|
|
||||||
|
# Check if already running
|
||||||
|
if pgrep -x "Xorg" >/dev/null 2>&1; then
|
||||||
|
echo "Xorg already running"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec /usr/lib/xorg/Xorg :0 \
|
||||||
|
-nolisten tcp \
|
||||||
|
-nolisten local \
|
||||||
|
-config /etc/X11/xorg.conf.d/99-dummy.conf \
|
||||||
|
+extension GLX \
|
||||||
|
+extension RANDR \
|
||||||
|
+extension RENDER \
|
||||||
|
-novtswitch \
|
||||||
|
-keeptty
|
||||||
23
config/vps/20-dummy-headless.conf
Normal file
23
config/vps/20-dummy-headless.conf
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Section "Device"
|
||||||
|
Identifier "DummyHeadless"
|
||||||
|
Driver "dummy"
|
||||||
|
Option "ConstantDPI" "true"
|
||||||
|
VideoRam 256000
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "Monitor"
|
||||||
|
Identifier "DummyMonitor"
|
||||||
|
HorizSync 28-80
|
||||||
|
VertRefresh 48-75
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "Screen"
|
||||||
|
Identifier "DummyScreen"
|
||||||
|
Device "DummyHeadless"
|
||||||
|
Monitor "DummyMonitor"
|
||||||
|
DefaultDepth 24
|
||||||
|
SubSection "Display"
|
||||||
|
Depth 24
|
||||||
|
Modes "1920x1080" "1280x720"
|
||||||
|
EndSubSection
|
||||||
|
EndSection
|
||||||
17
config/vps/rustdesk-dummy.service
Normal file
17
config/vps/rustdesk-dummy.service
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RustDesk with Dummy Display
|
||||||
|
After=xorg-dummy.service
|
||||||
|
Requires=xorg-dummy.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Environment="DISPLAY=:0"
|
||||||
|
Environment="XAUTHORITY=/root/.Xauthority"
|
||||||
|
ExecStartPre=/bin/sleep 3
|
||||||
|
ExecStart=/usr/share/rustdesk/rustdesk --server
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
34
config/vps/rustdesk-vps-start.sh
Normal file
34
config/vps/rustdesk-vps-start.sh
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# RustDesk VPS Headless Client Startup Script
|
||||||
|
# Starts Xvfb :99, waits for display, then launches RustDesk client via DBus
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Kill old Xvfb
|
||||||
|
pkill -9 Xvfb 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Start Xvfb
|
||||||
|
/usr/bin/Xvfb :99 -screen 0 1920x1080x24 -ac -nolisten tcp -nolisten unix &
|
||||||
|
XVFB_PID=$!
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Wait for display to be ready
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if xdpyinfo -display :99 > /dev/null 2>&1; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Verify display is up
|
||||||
|
if ! xdpyinfo -display :99 > /dev/null 2>&1; then
|
||||||
|
echo "ERROR: Xvfb failed to start on :99"
|
||||||
|
kill $XVFB_PID 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DISPLAY=:99
|
||||||
|
|
||||||
|
# Start RustDesk client inside DBus session (required for headless GUI apps)
|
||||||
|
exec /usr/bin/dbus-run-session -- /usr/share/rustdesk/rustdesk --server
|
||||||
19
config/vps/rustdesk-vps.service
Normal file
19
config/vps/rustdesk-vps.service
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RustDesk + XFCE Desktop on VPS
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=forking
|
||||||
|
User=root
|
||||||
|
Environment="DISPLAY=:99"
|
||||||
|
Environment="XAUTHORITY=/root/.Xauthority"
|
||||||
|
|
||||||
|
# Start Xvfb, wait for display, then launch XFCE + RustDesk
|
||||||
|
ExecStartPre=/bin/bash -c 'pkill -9 -f rustdesk 2>/dev/null || true; pkill -9 -f xfce4-session 2>/dev/null || true; sleep 1; pkill -9 Xvfb 2>/dev/null || true; rm -f /tmp/.X11-unix/X99; sleep 1'
|
||||||
|
ExecStart=/bin/bash -c 'Xvfb :99 -screen 0 1920x1080x24 -ac +extension RANDR > /dev/null 2>&1 &; sleep 3; for i in $(seq 1 30); do export DISPLAY=:99; export XAUTHORITY=/root/.Xauthority; xrandr --listmonitors > /dev/null 2>&1 && break; sleep 1; done; nohup dbus-run-session -- xfce4-session > /tmp/xfce-session.log 2>&1 &; sleep 5; nohup dbus-run-session -- /usr/share/rustdesk/rustdesk --server > /tmp/rustdesk-session.log 2>&1 &; sleep 2; exit 0'
|
||||||
|
ExecStop=/bin/bash -c 'pkill -9 -f rustdesk; pkill -9 -f xfce4-session; pkill -9 Xvfb; rm -rf /tmp/RustDesk'
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
51
config/vps/start-rd-desktop.sh
Normal file
51
config/vps/start-rd-desktop.sh
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# RustDesk + XFCE Desktop Auto-Start for VPS
|
||||||
|
# Systemd runs this script; script waits for Xvfb to exit
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Kill old
|
||||||
|
pkill -9 -f rustdesk 2>/dev/null || true
|
||||||
|
pkill -9 -f xfce4-session 2>/dev/null || true
|
||||||
|
pkill -9 Xvfb 2>/dev/null || true
|
||||||
|
rm -f /tmp/.X11-unix/X99 /tmp/.X*-lock
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Start Xvfb :99
|
||||||
|
Xvfb :99 -screen 0 1920x1080x24 -ac +extension RANDR +extension RENDER +extension GLX > /dev/null 2>&1 &
|
||||||
|
XVFB_PID=$!
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Wait for display ready
|
||||||
|
export DISPLAY=:99
|
||||||
|
export XAUTHORITY=/root/.Xauthority
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
xrandr --listmonitors > /dev/null 2>&1 && break
|
||||||
|
if ! kill -0 $XVFB_PID 2>/dev/null; then
|
||||||
|
echo "ERROR: Xvfb died"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
xrandr --listmonitors > /dev/null 2>&1 || { echo "ERROR: Display :99 not ready"; exit 1; }
|
||||||
|
|
||||||
|
# Start XFCE desktop in background
|
||||||
|
nohup dbus-run-session -- xfce4-session > /tmp/xfce-session.log 2>&1 &
|
||||||
|
echo "XFCE PID: $!"
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Wait for window manager
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
ps aux | grep xfwm4 | grep -v grep > /dev/null 2>&1 && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Start RustDesk client in background
|
||||||
|
nohup dbus-run-session -- /usr/share/rustdesk/rustdesk --server > /tmp/rustdesk-session.log 2>&1 &
|
||||||
|
echo "RustDesk PID: $!"
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Wait for Xvfb (main process) - systemd tracks this script
|
||||||
|
# If Xvfb dies, systemd will restart the service
|
||||||
|
wait $XVFB_PID
|
||||||
51
config/vps/start-xfce-rustdesk.sh
Normal file
51
config/vps/start-xfce-rustdesk.sh
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# RustDesk + XFCE Desktop Auto-Start for VPS
|
||||||
|
# Systemd runs this script; script waits for Xvfb to exit
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Kill old
|
||||||
|
pkill -9 -f rustdesk 2>/dev/null || true
|
||||||
|
pkill -9 -f xfce4-session 2>/dev/null || true
|
||||||
|
pkill -9 Xvfb 2>/dev/null || true
|
||||||
|
rm -f /tmp/.X11-unix/X99 /tmp/.X*-lock
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Start Xvfb :99
|
||||||
|
Xvfb :99 -screen 0 1920x1080x24 -ac +extension RANDR +extension RENDER +extension GLX > /dev/null 2>&1 &
|
||||||
|
XVFB_PID=$!
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Wait for display ready
|
||||||
|
export DISPLAY=:99
|
||||||
|
export XAUTHORITY=/root/.Xauthority
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
xrandr --listmonitors > /dev/null 2>&1 && break
|
||||||
|
if ! kill -0 $XVFB_PID 2>/dev/null; then
|
||||||
|
echo "ERROR: Xvfb died"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
xrandr --listmonitors > /dev/null 2>&1 || { echo "ERROR: Display :99 not ready"; exit 1; }
|
||||||
|
|
||||||
|
# Start XFCE desktop in background
|
||||||
|
nohup dbus-run-session -- xfce4-session > /tmp/xfce-session.log 2>&1 &
|
||||||
|
echo "XFCE PID: $!"
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Wait for window manager
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
ps aux | grep xfwm4 | grep -v grep > /dev/null 2>&1 && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Start RustDesk client in background
|
||||||
|
nohup dbus-run-session -- /usr/share/rustdesk/rustdesk --server > /tmp/rustdesk-session.log 2>&1 &
|
||||||
|
echo "RustDesk PID: $!"
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Wait for Xvfb (main process) - systemd tracks this script
|
||||||
|
# If Xvfb dies, systemd will restart the service
|
||||||
|
wait $XVFB_PID
|
||||||
16
config/vps/xorg-dummy.service
Normal file
16
config/vps/xorg-dummy.service
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Xorg Dummy Display
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Environment="DISPLAY=:0"
|
||||||
|
ExecStartPre=/bin/bash -c 'test -f /root/.Xauthority || (xauth -f /root/.Xauthority add :0 . $(mcookie) \u0026\u0026 chmod 600 /root/.Xauthority)'
|
||||||
|
ExecStartPre=/bin/mkdir -p /tmp/.X11-unix
|
||||||
|
ExecStart=/usr/lib/xorg/Xorg :0 -auth /root/.Xauthority -nolisten tcp -config /etc/X11/xorg.conf.d/20-dummy-headless.conf +extension GLX +extension RANDR +extension RENDER -novtswitch -keeptty
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
23
config/x11-host/20-dummy-headless.conf
Normal file
23
config/x11-host/20-dummy-headless.conf
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Section "Device"
|
||||||
|
Identifier "DummyHeadless"
|
||||||
|
Driver "dummy"
|
||||||
|
Option "ConstantDPI" "true"
|
||||||
|
VideoRam 256000
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "Monitor"
|
||||||
|
Identifier "DummyMonitor"
|
||||||
|
HorizSync 28-80
|
||||||
|
VertRefresh 48-75
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "Screen"
|
||||||
|
Identifier "DummyScreen"
|
||||||
|
Device "DummyHeadless"
|
||||||
|
Monitor "DummyMonitor"
|
||||||
|
DefaultDepth 24
|
||||||
|
SubSection "Display"
|
||||||
|
Depth 24
|
||||||
|
Modes "1920x1080" "1280x720"
|
||||||
|
EndSubSection
|
||||||
|
EndSection
|
||||||
9
config/x11-host/20-intel-virtual.conf.bak
Normal file
9
config/x11-host/20-intel-virtual.conf.bak
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# BACKUP of original Intel VirtualHeads config
|
||||||
|
# Disabled in favour of dummy headless config (20-dummy-headless.conf)
|
||||||
|
# which works without HDMI connected at boot.
|
||||||
|
#
|
||||||
|
# Section "Device"
|
||||||
|
# Identifier "Intel Graphics"
|
||||||
|
# Driver "intel"
|
||||||
|
# Option "VirtualHeads" "1"
|
||||||
|
# EndSection
|
||||||
9
configs/rustdesk2-host.toml.template
Normal file
9
configs/rustdesk2-host.toml.template
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# RustDesk2.toml для хоста Intel Alder Lake-N
|
||||||
|
# Публичный сервер для стабильного ID
|
||||||
|
rendezvous_server = 'rs-ny.rustdesk.com:21116'
|
||||||
|
nat_type = 1
|
||||||
|
serial = 0
|
||||||
|
unlock_pin = ''
|
||||||
|
|
||||||
|
[options]
|
||||||
|
local-ip-addr = '<IP хоста>'
|
||||||
17
configs/systemd/rustdesk-client-local.service
Normal file
17
configs/systemd/rustdesk-client-local.service
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RustDesk Client (local hbbs)
|
||||||
|
After=xorg-dummy.service network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Environment="DISPLAY=:0"
|
||||||
|
Environment="XAUTHORITY=/root/.Xauthority"
|
||||||
|
ExecStartPre=/root/write-rustdesk-config.sh
|
||||||
|
ExecStartPre=/bin/sleep 2
|
||||||
|
ExecStart=/usr/share/rustdesk/rustdesk --server
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
23
configs/systemd/rustdesk-hbbr.service
Normal file
23
configs/systemd/rustdesk-hbbr.service
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RustDesk Server (hbbr — relay)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/hbbr
|
||||||
|
WorkingDirectory=/var/lib/rustdesk-server
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/var/lib/rustdesk-server
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=rustdesk-hbbr
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
23
configs/systemd/rustdesk-hbbs.service
Normal file
23
configs/systemd/rustdesk-hbbs.service
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RustDesk Server (hbbs — ID / NAT relay)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/hbbs
|
||||||
|
WorkingDirectory=/var/lib/rustdesk-server
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/var/lib/rustdesk-server
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=rustdesk-hbbs
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
22
docker-compose.vps.yml
Normal file
22
docker-compose.vps.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
hbbs:
|
||||||
|
image: rustdesk/rustdesk-server:latest
|
||||||
|
container_name: rustdesk-hbbs
|
||||||
|
command: hbbs -r rustdesk-hbbr:21117
|
||||||
|
volumes:
|
||||||
|
- ./data:/root
|
||||||
|
network_mode: host
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- hbbr
|
||||||
|
|
||||||
|
hbbr:
|
||||||
|
image: rustdesk/rustdesk-server:latest
|
||||||
|
container_name: rustdesk-hbbr
|
||||||
|
command: hbbr
|
||||||
|
volumes:
|
||||||
|
- ./data:/root
|
||||||
|
network_mode: host
|
||||||
|
restart: unless-stopped
|
||||||
219
install.sh
219
install.sh
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# RDtop Installer — RustDesk headless display setup
|
# RDtop Installer — RustDesk headless display setup (dummy driver)
|
||||||
# Usage: bash -c "$(curl -fsSL https://git.softuniq.eu/NW/RDtop/raw/branch/main/install.sh)"
|
# Usage: bash -c "$(curl -fsSL https://git.softuniq.eu/NW/RDtop/raw/branch/main/install.sh)"
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -8,9 +8,10 @@ echo "Installing RustDesk headless display fallback..."
|
|||||||
|
|
||||||
# Config paths
|
# Config paths
|
||||||
REPO_URL="https://git.softuniq.eu/NW/RDtop"
|
REPO_URL="https://git.softuniq.eu/NW/RDtop"
|
||||||
X11_CONF="/etc/X11/xorg.conf.d/90-fallback.conf"
|
X11_CONF="/etc/X11/xorg.conf.d/20-dummy-headless.conf"
|
||||||
BIN_DST="$HOME/.local/bin/hdmi-fallback.sh"
|
BIN_DST="$HOME/.local/bin/hdmi-fallback.sh"
|
||||||
SYSTEMD_DST="$HOME/.config/systemd/user/hdmi-fallback.service"
|
SYSTEMD_DST="$HOME/.config/systemd/user/hdmi-fallback.service"
|
||||||
|
EDID_DST="/lib/firmware/edid/samsung.bin"
|
||||||
LOG="$HOME/.local/logs/rdtop-install.log"
|
LOG="$HOME/.local/logs/rdtop-install.log"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$LOG")"
|
mkdir -p "$(dirname "$LOG")"
|
||||||
@@ -24,130 +25,203 @@ if [ "$EUID" -eq 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! xset q >/dev/null 2>&1; then
|
# --- Check sudo works ---
|
||||||
log "WARNING: No X11 display found. You need to run this from a graphical session."
|
if ! sudo -n true 2>/dev/null; then
|
||||||
|
echo "This script requires sudo privileges. Please enter your password when prompted."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Check if dummy driver exists or can be installed ---
|
# --- Install dummy driver ---
|
||||||
if [ -f /usr/lib/xorg/modules/drivers/dummy_drv.so ]; then
|
if [ -f /usr/lib/xorg/modules/drivers/dummy_drv.so ]; then
|
||||||
log "Dummy driver already present"
|
log "Dummy driver already present"
|
||||||
else
|
else
|
||||||
log "Dummy driver not found. Will try to install xserver-xorg-video-dummy..."
|
log "Installing xserver-xorg-video-dummy..."
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq xserver-xorg-video-dummy || log "WARNING: apt failed, dummy not installed (VIRTUAL1 should still work)"
|
sudo apt-get update -qq && sudo apt-get install -y -qq xserver-xorg-video-dummy || true
|
||||||
else
|
else
|
||||||
log "WARNING: apt-get not found, skipping dummy driver install"
|
log "WARNING: apt-get not found, cannot install dummy driver"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Create X11 fallback config ---
|
# --- Remove old conflicting configs ---
|
||||||
log "Creating X11 fallback config at $X11_CONF"
|
log "Removing old Intel X11 configs"
|
||||||
echo "retrowest" | sudo -S bash -c 'cat > /etc/X11/xorg.conf.d/90-fallback.conf' <<'EOF'
|
sudo rm -f /etc/X11/xorg.conf.d/20-intel-virtual.conf
|
||||||
# Fallback Xorg config: guarantee a framebuffer even without HDMI
|
sudo rm -f /etc/X11/xorg.conf.d/90-fallback.conf
|
||||||
|
|
||||||
|
# --- Copy EDID firmware ---
|
||||||
|
log "Saving EDID firmware"
|
||||||
|
if [ -f /sys/class/drm/card0-HDMI-A-1/edid ]; then
|
||||||
|
sudo mkdir -p /lib/firmware/edid
|
||||||
|
sudo cp /sys/class/drm/card0-HDMI-A-1/edid "$EDID_DST"
|
||||||
|
sudo chmod 644 "$EDID_DST"
|
||||||
|
log "EDID saved to $EDID_DST"
|
||||||
|
else
|
||||||
|
log "WARNING: No EDID found at /sys/class/drm/card0-HDMI-A-1/edid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Create dummy headless X11 config ---
|
||||||
|
log "Creating dummy headless X11 config at $X11_CONF"
|
||||||
|
cat <<'XORG' | sudo tee "$X11_CONF" >/dev/null
|
||||||
Section "Device"
|
Section "Device"
|
||||||
Identifier "AutoDevice"
|
Identifier "DummyHeadless"
|
||||||
Driver "modesetting"
|
Driver "dummy"
|
||||||
|
Option "ConstantDPI" "true"
|
||||||
|
VideoRam 256000
|
||||||
EndSection
|
EndSection
|
||||||
|
|
||||||
Section "Monitor"
|
Section "Monitor"
|
||||||
Identifier "AutoMonitor"
|
Identifier "DummyMonitor"
|
||||||
Option "DPMS" "false"
|
HorizSync 28-80
|
||||||
|
VertRefresh 48-75
|
||||||
EndSection
|
EndSection
|
||||||
|
|
||||||
Section "Screen"
|
Section "Screen"
|
||||||
Identifier "AutoScreen"
|
Identifier "DummyScreen"
|
||||||
Device "AutoDevice"
|
Device "DummyHeadless"
|
||||||
Monitor "AutoMonitor"
|
Monitor "DummyMonitor"
|
||||||
DefaultDepth 24
|
DefaultDepth 24
|
||||||
SubSection "Display"
|
SubSection "Display"
|
||||||
Depth 24
|
Depth 24
|
||||||
Virtual 1920 1080
|
Modes "1920x1080" "1280x720"
|
||||||
Modes "1920x1080"
|
|
||||||
EndSubSection
|
EndSubSection
|
||||||
EndSection
|
EndSection
|
||||||
EOF
|
XORG
|
||||||
|
sudo chmod 644 "$X11_CONF"
|
||||||
sudo chmod 644 /etc/X11/xorg.conf.d/90-fallback.conf
|
|
||||||
log "X11 config installed"
|
log "X11 config installed"
|
||||||
|
|
||||||
|
# --- Update GRUB for kernel EDID fallback ---
|
||||||
|
log "Updating GRUB cmdline for EDID fallback"
|
||||||
|
if [ -f /etc/default/grub ]; then
|
||||||
|
sudo sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=".*"|GRUB_CMDLINE_LINUX_DEFAULT="quiet splash video=HDMI-A-1:1920x1080@60 drm.edid_firmware=HDMI-A-1:edid/samsung.bin"|' /etc/default/grub
|
||||||
|
sudo update-grub >/dev/null 2>&1 || log "WARNING: update-grub failed"
|
||||||
|
log "GRUB updated"
|
||||||
|
else
|
||||||
|
log "WARNING: /etc/default/grub not found"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Install fallback script ---
|
# --- Install fallback script ---
|
||||||
log "Installing hdmi-fallback.sh to ~/.local/bin/"
|
log "Installing hdmi-fallback.sh to ~/.local/bin/"
|
||||||
mkdir -p "$HOME/.local/bin"
|
mkdir -p "$HOME/.local/bin"
|
||||||
cat > "$BIN_DST" <<'SCRIPT'
|
cat > "$BIN_DST" <<'SCRIPT'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# Universal HDMI Fallback: DUMMY0 (dummy driver) or VIRTUAL1 (intel)
|
||||||
|
# Works both with HDMI connected and disconnected at boot
|
||||||
|
|
||||||
LOG="$HOME/.local/logs/drm-hotplug.log"
|
LOG="$HOME/.local/logs/drm-hotplug.log"
|
||||||
mkdir -p "$(dirname "$LOG")"
|
mkdir -p "$HOME/.local/logs"
|
||||||
export DISPLAY=:0
|
export DISPLAY=:0
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG" >&1
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG" >/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
log "=== Starting VIRTUAL1-primary fallback monitor ==="
|
# Find available primary output (DUMMY0 or VIRTUAL1)
|
||||||
|
find_primary_output() {
|
||||||
MODE="1920x1080_60.00"
|
if xrandr 2>/dev/null | grep -q "^DUMMY0"; then
|
||||||
if ! xrandr 2>/dev/null | grep -q "$MODE"; then
|
echo "DUMMY0"
|
||||||
xrandr --newmode "$MODE" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 2>/dev/null || true
|
elif xrandr 2>/dev/null | grep -q "^VIRTUAL1"; then
|
||||||
fi
|
echo "VIRTUAL1"
|
||||||
xrandr --addmode VIRTUAL1 "$MODE" 2>/dev/null || true
|
else
|
||||||
|
echo ""
|
||||||
setup_virtual_primary() {
|
fi
|
||||||
log "Setting up VIRTUAL1 as primary..."
|
|
||||||
xrandr --output VIRTUAL1 --mode "$MODE" --primary 2>&1 && log "VIRTUAL1 primary OK" || log "VIRTUAL1 primary failed"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enable_hdmi_clone() {
|
# Find HDMI output name (HDMI-1, HDMI1, etc)
|
||||||
log "Enabling HDMI1 as clone of VIRTUAL1"
|
find_hdmi_output() {
|
||||||
xrandr --output HDMI1 --auto --same-as VIRTUAL1 2>&1 && log "HDMI1 cloned OK" || log "HDMI1 clone failed"
|
xrandr 2>/dev/null | grep -E "^HDMI-[0-9] connected" | awk '{print $1}' | head -1
|
||||||
}
|
}
|
||||||
|
|
||||||
disable_hdmi() {
|
# Set up mode for virtual output
|
||||||
log "HDMI1 disconnected, keeping VIRTUAL1 primary"
|
setup_mode() {
|
||||||
xrandr --output HDMI1 --off 2>&1 && log "HDMI1 off OK" || true
|
local OUTPUT="$1"
|
||||||
|
local MODE_NAME="1920x1080"
|
||||||
|
|
||||||
|
if [ "$OUTPUT" = "VIRTUAL1" ]; then
|
||||||
|
local MODELINE="1920x1080_60.00"
|
||||||
|
if ! xrandr 2>/dev/null | grep -q "$MODELINE"; then
|
||||||
|
xrandr --newmode "$MODELINE" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
xrandr --addmode VIRTUAL1 "$MODELINE" 2>/dev/null || true
|
||||||
|
MODE_NAME="$MODELINE"
|
||||||
|
fi
|
||||||
|
echo "$MODE_NAME"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log "Start monitor (user=$USER, display=$DISPLAY)"
|
||||||
|
|
||||||
|
CONNECTOR=""
|
||||||
for p in /sys/class/drm/card1-HDMI-A-1 /sys/class/drm/card0-HDMI-A-1; do
|
for p in /sys/class/drm/card1-HDMI-A-1 /sys/class/drm/card0-HDMI-A-1; do
|
||||||
[ -e "$p/status" ] && { CONNECTOR="$p"; break; }
|
[ -e "$p/status" ] && { CONNECTOR="$p"; break; }
|
||||||
done
|
done
|
||||||
[ -z "$CONNECTOR" ] && CONNECTOR=$(find /sys/class/drm -maxdepth 1 -name "*HDMI-A-1" -type d | head -1)
|
[ -z "$CONNECTOR" ] && CONNECTOR=$(find /sys/class/drm -maxdepth 1 -name "*HDMI-A-1" -type d | head -1)
|
||||||
|
|
||||||
if [ -z "$CONNECTOR" ] || [ ! -e "$CONNECTOR/status" ]; then
|
if [ -z "$CONNECTOR" ] || [ ! -e "$CONNECTOR/status" ]; then
|
||||||
log "ERROR: No HDMI-A-1 found"
|
log "WARNING: No HDMI-A-1 connector found, running headless only"
|
||||||
setup_virtual_primary
|
fi
|
||||||
|
|
||||||
|
# Initial state
|
||||||
|
LAST_STATE=$(cat "$CONNECTOR/status" 2>/dev/null || echo unknown)
|
||||||
|
log "Initial HDMI: $LAST_STATE"
|
||||||
|
|
||||||
|
# Detect outputs
|
||||||
|
PRIMARY=$(find_primary_output)
|
||||||
|
HDMI=$(find_hdmi_output)
|
||||||
|
|
||||||
|
if [ -z "$PRIMARY" ]; then
|
||||||
|
log "ERROR: No virtual output found (DUMMY0 or VIRTUAL1)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Monitoring: $CONNECTOR"
|
log "Primary: $PRIMARY, HDMI: ${HDMI:-none}"
|
||||||
|
|
||||||
setup_virtual_primary
|
MODE=$(setup_mode "$PRIMARY")
|
||||||
HDMI_STATE=$(cat "$CONNECTOR/status" 2>/dev/null || echo "unknown")
|
|
||||||
log "Initial HDMI state: $HDMI_STATE"
|
|
||||||
|
|
||||||
if [ "$HDMI_STATE" = "connected" ]; then
|
# Initial setup
|
||||||
enable_hdmi_clone
|
if [ "$LAST_STATE" = "connected" ] && [ -n "$HDMI" ]; then
|
||||||
|
log "HDMI connected, setting clone: $PRIMARY primary + $HDMI same-as"
|
||||||
|
xrandr --output "$PRIMARY" --mode "$MODE" --primary 2>/dev/null || true
|
||||||
|
xrandr --output "$HDMI" --auto --same-as "$PRIMARY" 2>/dev/null || true
|
||||||
else
|
else
|
||||||
disable_hdmi
|
log "HDMI disconnected or not found, keeping $PRIMARY primary"
|
||||||
|
xrandr --output "$PRIMARY" --mode "$MODE" --primary 2>/dev/null || true
|
||||||
|
[ -n "$HDMI" ] && xrandr --output "$HDMI" --off 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Initial setup complete. Monitors:"
|
log "Setup complete. Monitors:"
|
||||||
xrandr --listmonitors 2>&1 | while read line; do log " $line"; done
|
xrandr --listmonitors 2>/dev/null | while read -r line; do log " $line"; done
|
||||||
|
|
||||||
LAST_STATE="$HDMI_STATE"
|
|
||||||
|
|
||||||
|
# Main loop
|
||||||
while true; do
|
while true; do
|
||||||
CURRENT=$(cat "$CONNECTOR/status" 2>/dev/null || echo "unknown")
|
CURRENT=$(cat "$CONNECTOR/status" 2>/dev/null || echo unknown)
|
||||||
|
|
||||||
if [ "$CURRENT" != "$LAST_STATE" ]; then
|
if [ "$CURRENT" != "$LAST_STATE" ]; then
|
||||||
log "HDMI changed: $LAST_STATE -> $CURRENT"
|
log "HDMI changed: $LAST_STATE -> $CURRENT"
|
||||||
setup_virtual_primary
|
|
||||||
if [ "$CURRENT" = "connected" ]; then
|
# Re-detect
|
||||||
enable_hdmi_clone
|
PRIMARY=$(find_primary_output)
|
||||||
else
|
HDMI=$(find_hdmi_output)
|
||||||
disable_hdmi
|
MODE=$(setup_mode "$PRIMARY")
|
||||||
|
|
||||||
|
if [ -z "$PRIMARY" ]; then
|
||||||
|
log "ERROR: No virtual output detected after HDMI change"
|
||||||
|
sleep 5
|
||||||
|
continue
|
||||||
fi
|
fi
|
||||||
log "Monitor layout after change:"
|
|
||||||
xrandr --listmonitors 2>&1 | while read line; do log " $line"; done
|
# Always make PRIMARY primary
|
||||||
|
xrandr --output "$PRIMARY" --mode "$MODE" --primary 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ "$CURRENT" = "connected" ] && [ -n "$HDMI" ]; then
|
||||||
|
log "HDMI connected, cloning $HDMI to $PRIMARY"
|
||||||
|
xrandr --output "$HDMI" --auto --same-as "$PRIMARY" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
log "HDMI disconnected, keeping $PRIMARY primary"
|
||||||
|
[ -n "$HDMI" ] && xrandr --output "$HDMI" --off 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
LAST_STATE="$CURRENT"
|
LAST_STATE="$CURRENT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
SCRIPT
|
SCRIPT
|
||||||
@@ -160,7 +234,7 @@ log "Installing systemd user unit"
|
|||||||
mkdir -p "$HOME/.config/systemd/user"
|
mkdir -p "$HOME/.config/systemd/user"
|
||||||
cat > "$SYSTEMD_DST" <<'UNIT'
|
cat > "$SYSTEMD_DST" <<'UNIT'
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=HDMI to VIRTUAL1 Fallback Monitor
|
Description=HDMI to Virtual Fallback Monitor
|
||||||
After=graphical-session.target
|
After=graphical-session.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@@ -175,8 +249,8 @@ WantedBy=graphical-session.target
|
|||||||
UNIT
|
UNIT
|
||||||
|
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable --now hdmi-fallback.service
|
systemctl --user enable --now hdmi-fallback.service 2>/dev/null || true
|
||||||
log "Systemd unit enabled and started"
|
log "Systemd unit enabled"
|
||||||
|
|
||||||
# --- Summary ---
|
# --- Summary ---
|
||||||
echo ""
|
echo ""
|
||||||
@@ -185,15 +259,16 @@ echo "RDtop installation complete!"
|
|||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo "1. Reboot or re-login to apply X11 config"
|
echo "1. REBOOT now for changes to take effect:"
|
||||||
echo " sudo systemctl restart gdm"
|
echo " sudo reboot"
|
||||||
echo ""
|
echo ""
|
||||||
echo "2. After reboot, verify VIRTUAL1 is primary:"
|
echo "2. After reboot without HDMI, check:"
|
||||||
echo " xrandr --listmonitors"
|
echo " xrandr --listmonitors"
|
||||||
echo ""
|
echo ""
|
||||||
echo "3. Check service status:"
|
echo "3. Service status:"
|
||||||
echo " systemctl --user status hdmi-fallback.service"
|
echo " systemctl --user status hdmi-fallback.service"
|
||||||
echo ""
|
echo ""
|
||||||
echo "4. To verify: unplug HDMI cable, VIRTUAL1 will stay active"
|
echo "4. View logs:"
|
||||||
|
echo " cat ~/.local/logs/drm-hotplug.log"
|
||||||
echo ""
|
echo ""
|
||||||
log "Install complete"
|
log "Install complete"
|
||||||
|
|||||||
56
scripts/README.md
Normal file
56
scripts/README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# RustDesk Server — Нативная установка на VPS
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
| Файл | Назначение |
|
||||||
|
|------|------------|
|
||||||
|
| `install-rustdesk-server.sh` | Скачивает и устанавливает hbbs + hbbr |
|
||||||
|
| `rustdesk-hbbs.service` | systemd unit для ID-сервера |
|
||||||
|
| `rustdesk-hbbr.service` | systemd unit для relay |
|
||||||
|
| `open-rustdesk-ports.sh` | Открывает порты в UFW |
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Скопируй файлы на VPS
|
||||||
|
scp install-rustdesk-server.sh \
|
||||||
|
rustdesk-hbbs.service \
|
||||||
|
rustdesk-hbbr.service \
|
||||||
|
open-rustdesk-ports.sh \
|
||||||
|
root@2.59.219.61:/root/
|
||||||
|
|
||||||
|
# 2. Зайди на VPS
|
||||||
|
ssh root@2.59.219.61
|
||||||
|
|
||||||
|
# 3. Запусти
|
||||||
|
chmod +x install-rustdesk-server.sh open-rustdesk-ports.sh
|
||||||
|
./open-rustdesk-ports.sh
|
||||||
|
./install-rustdesk-server.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Порты
|
||||||
|
|
||||||
|
| Порт | Протокол | Назначение |
|
||||||
|
|------|----------|------------|
|
||||||
|
| 21115 | TCP | NAT type detection |
|
||||||
|
| 21116 | TCP + UDP | ID registration / hole punching |
|
||||||
|
| 21117 | TCP | Relay |
|
||||||
|
| 21118 | TCP | Web console (опционально) |
|
||||||
|
| 21119 | TCP | Web console HTTPS (опционально) |
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status rustdesk-hbbs rustdesk-hbbr
|
||||||
|
ss -tlnp | grep -E '21115|21116|21117'
|
||||||
|
journalctl -u rustdesk-hbbs -f
|
||||||
|
cat /var/lib/rustdesk-server/id_ed25519.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
## Настройка клиента
|
||||||
|
|
||||||
|
1. Открой RustDesk на VPS
|
||||||
|
2. Нажми «Настройки сети» → «ID-сервер»
|
||||||
|
3. Вставь IP VPS и порт: `2.59.219.61:21116`
|
||||||
|
4. Вставь ключ из `/var/lib/rustdesk-server/id_ed25519.pub`
|
||||||
|
5. Готово — ID теперь локальный и постоянный
|
||||||
146
scripts/host/README-host.md
Normal file
146
scripts/host/README-host.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# RustDesk Host Setup — README
|
||||||
|
|
||||||
|
**Цель:** зафиксировать RustDesk ID на хосте Intel Alder Lake-N (Debian 12, GNOME headless, dummy-драйвер) и защитить конфиг от перезаписи.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Содержимое
|
||||||
|
|
||||||
|
| Файл | Назначение |
|
||||||
|
|------|-----------|
|
||||||
|
| `setup-host-rustdesk.sh` | Основной скрипт: проверяет dummy Xorg, создаёт/защищает `RustDesk.toml`, создаёт `write-rustdesk-config.sh`, настраивает systemd-сервис |
|
||||||
|
| `write-rustdesk-config.sh` | Экстренный скрипт: снимает `chattr +i`, перезаписывает конфиг, ставит защиту обратно |
|
||||||
|
| `setup-host-rustdesk.log` | Лог, создаётся автоматически при запуске основного скрипта |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как применить
|
||||||
|
|
||||||
|
### Шаг 1: Скопировать файлы на хост
|
||||||
|
|
||||||
|
**Вариант A: USB / вручную**
|
||||||
|
```bash
|
||||||
|
# На VPS (где вы сейчас)
|
||||||
|
# Файлы уже находятся в папке scripts/host/. Скопируйте их на USB и перенесите на хост.
|
||||||
|
# Или если есть любой файловый доступ — просто перенесите.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Вариант B: scp (если есть любой сетевой доступ к хосту)**
|
||||||
|
```bash
|
||||||
|
# С хоста или откуда есть доступ:
|
||||||
|
scp setup-host-rustdesk.sh user@host-ip:/tmp/
|
||||||
|
ssh user@host-ip "sudo bash /tmp/setup-host-rustdesk.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Вариант C: curl через временный HTTP-сервер**
|
||||||
|
```bash
|
||||||
|
# На промежуточной машине:
|
||||||
|
python3 -m http.server 8080 --directory /path/to/scripts/host/
|
||||||
|
|
||||||
|
# На хосте:
|
||||||
|
curl -O http://PROMO-IP:8080/setup-host-rustdesk.sh
|
||||||
|
sudo bash setup-host-rustdesk.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 2: Запустить основной скрипт
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash setup-host-rustdesk.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт должен выполниться полностью без ошибок.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что проверить после применения
|
||||||
|
|
||||||
|
### 1. RustDesk ID
|
||||||
|
```bash
|
||||||
|
rustdesk --get-id
|
||||||
|
```
|
||||||
|
Ожидаемый результат: 9-значный номер (например, `458564614` на VPS). На хосте будет **другой** ID — это нормально. Главное, чтобы он **не менялся после перезагрузки**.
|
||||||
|
|
||||||
|
### 2. Конфиг защищён
|
||||||
|
```bash
|
||||||
|
lsattr /home/ИМЯПОЛЬЗОВАТЕЛЯ/.config/rustdesk/RustDesk.toml
|
||||||
|
```
|
||||||
|
Ожидаемый результат: флаг `i` (immutable), например:
|
||||||
|
```
|
||||||
|
---- i --------- /home/user/.config/rustdesk/RustDesk.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Сервис активен
|
||||||
|
```bash
|
||||||
|
systemctl is-active rustdesk-client-local
|
||||||
|
```
|
||||||
|
Ожидаемый результат: `active`
|
||||||
|
|
||||||
|
### 4. Dummy монитор работает
|
||||||
|
```bash
|
||||||
|
DISPLAY=:1 xrandr --listmonitors
|
||||||
|
```
|
||||||
|
Ожидаемый результат: строка с `DUMMY0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что делать, если ID всё равно меняется
|
||||||
|
|
||||||
|
### Вариант 1: RustDesk не успел сгенерировать ID перед chattr
|
||||||
|
Если ID меняется, скорее всего RustDesk создал новый файл конфигурации в другом месте или перезаписал его через другой процесс.
|
||||||
|
|
||||||
|
**Проверьте:**
|
||||||
|
```bash
|
||||||
|
find / -name "RustDesk.toml" 2>/dev/null
|
||||||
|
```
|
||||||
|
Если найдено несколько — значит RustDesk использует не тот.
|
||||||
|
|
||||||
|
Правильный: `/home/ИМЯПОЛЬЗОВАТЕЛЯ/.config/rustdesk/RustDesk.toml`
|
||||||
|
|
||||||
|
### Вариант 2: RustDesk запущен без DISPLAY
|
||||||
|
Для генерации ID RustDesk **должен** запуститься с GUI (даже dummy). Убедитесь, что `xorg-dummy.service` запущен **до** rustdesk-client-local:
|
||||||
|
```bash
|
||||||
|
systemctl status xorg-dummy
|
||||||
|
systemctl status rustdesk-client-local
|
||||||
|
```
|
||||||
|
Если `xorg-dummy` неактивен — RustDesk может работать в CLI-режиме и генерировать новый ID каждый раз.
|
||||||
|
|
||||||
|
### Вариант 3: Файл отсутствовал при первом запуске скрипта
|
||||||
|
Если вы увидели в логе:
|
||||||
|
```
|
||||||
|
WARN: RustDesk.toml ОТСУТСТВУЕТ, и rustdesk --get-id не вернул ID.
|
||||||
|
```
|
||||||
|
Нужно:
|
||||||
|
1. Запустить RustDesk вручную с GUI один раз (чтобы он создал конфиг)
|
||||||
|
2. Затем применить защиту:
|
||||||
|
```bash
|
||||||
|
sudo chattr -i /home/user/.config/rustdesk/RustDesk.toml # если уже ставили
|
||||||
|
sudo chattr +i /home/user/.config/rustdesk/RustDesk.toml # защитить
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Предупреждения
|
||||||
|
|
||||||
|
⚠️ **chattr +i** блокирует **любую** запись в файл, в том числе от RustDesk. Это цель — зафиксировать ID. Но если нужно поменять сервер или другие настройки — используйте `write-rustdesk-config.sh`.
|
||||||
|
|
||||||
|
⚠️ Скрипт полагается на то, что:
|
||||||
|
- `rustdesk` установлен и доступен в `/usr/bin/rustdesk`
|
||||||
|
- Пользователь, под которым работает GNOME, называется `user` (или задайте `USER=... USER_HOME=...` перед запуском)
|
||||||
|
- Dummy-драйвер уже сконфигурирован (из предыдущих сессий)
|
||||||
|
|
||||||
|
⚠️ **Экстренный write-скрипт**:
|
||||||
|
```bash
|
||||||
|
sudo bash write-rustdesk-config.sh myusername
|
||||||
|
```
|
||||||
|
Он временно снимает `chattr +i`, перезаписывает файл и ставит защиту обратно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Логи
|
||||||
|
|
||||||
|
Все действия основного скрипта пишутся в:
|
||||||
|
```
|
||||||
|
./setup-host-rustdesk.log
|
||||||
|
```
|
||||||
|
|
||||||
|
При возникновении проблем — приложите этот файл.
|
||||||
43
scripts/host/manual-start-rustdesk.sh
Normal file
43
scripts/host/manual-start-rustdesk.sh
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# RustDesk Client Launcher for VPS (Manual Run)
|
||||||
|
# Run this script when you want to connect to host from VPS
|
||||||
|
|
||||||
|
# 1. Ensure no conflicting X servers
|
||||||
|
pkill -f Xvfb
|
||||||
|
pkill -f Xorg
|
||||||
|
|
||||||
|
# 2. Start Xvfb on :0
|
||||||
|
rm -f /tmp/.X11-unix/X0 /tmp/.X0-lock 2>/dev/null
|
||||||
|
/usr/bin/Xvfb :0 -screen 0 1920x1080x24 +extension GLX +extension RANDR +extension RENDER -ac &
|
||||||
|
echo "Xvfb started on :0"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 3. Configure RustDesk
|
||||||
|
cat > ~/.config/rustdesk/RustDesk2.toml <<'EOF'
|
||||||
|
rendezvous_server = 'localhost:21116'
|
||||||
|
custom-rendezvous-server = 'localhost:21116'
|
||||||
|
key = 'JDSXd9sxN6Y7mIzu3krSpPTTc4yjChfqpzgIH5fl9Iw='
|
||||||
|
nat_type = 1
|
||||||
|
serial = 0
|
||||||
|
unlock_pin = ''
|
||||||
|
|
||||||
|
[options]
|
||||||
|
local-ip-addr = '2.59.219.61'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 4. Start RustDesk client
|
||||||
|
export DISPLAY=:0
|
||||||
|
export XAUTHORITY=/root/.Xauthority
|
||||||
|
export LIBVA_DRIVER_NAME=none
|
||||||
|
export VDPAU_DRIVER=none
|
||||||
|
|
||||||
|
# Add xauth for display :0
|
||||||
|
xauth -f "$XAUTHORITY" add :0 . $(mcookie) 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Starting RustDesk..."
|
||||||
|
nohup /usr/share/rustdesk/rustdesk &
|
||||||
|
|
||||||
|
echo "RustDesk started. Check ID: DISPLAY=:0 /usr/share/rustdesk/rustdesk --get-id"
|
||||||
|
sleep 2
|
||||||
|
echo "Current ID:"
|
||||||
|
DISPLAY=:0 /usr/share/rustdesk/rustdesk --get-id
|
||||||
207
scripts/host/setup-host-rustdesk.sh
Normal file
207
scripts/host/setup-host-rustdesk.sh
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# setup-host-rustdesk.sh
|
||||||
|
# Защита RustDesk ID на хосте Intel Alder Lake-N
|
||||||
|
# Запускать от root
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LOG="$SCRIPT_DIR/setup-host-rustdesk.log"
|
||||||
|
USER_HOME="${USER_HOME:-/home/user}"
|
||||||
|
USER="${USER:-user}"
|
||||||
|
RUSTDESK_DIR="$USER_HOME/.config/rustdesk"
|
||||||
|
CONFIG_FILE="$RUSTDESK_DIR/RustDesk.toml"
|
||||||
|
|
||||||
|
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG"; }
|
||||||
|
warn() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $1" | tee -a "$LOG" >&2; }
|
||||||
|
fail() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] FATAL: $1" | tee -a "$LOG" >&2; exit 1; }
|
||||||
|
|
||||||
|
[[ $EUID -ne 0 ]] && fail "Этот скрипт нужно запускать от root: sudo bash setup-host-rustdesk.sh"
|
||||||
|
|
||||||
|
log "========== Начало настройки RustDesk на хосте =========="
|
||||||
|
log "USER_HOME=$USER_HOME | USER=$USER"
|
||||||
|
|
||||||
|
|
||||||
|
# 1. Проверить dummy Xorg (GNOME с dummy-драйвером должен показывать монитор)
|
||||||
|
log "--- Шаг 1: Проверка dummy Xorg ---"
|
||||||
|
# systemd-сервис xorg-dummy уже должен быть создан пользователем или предыдущими сессиями
|
||||||
|
if systemctl is-active --quiet xorg-dummy 2>/dev/null; then
|
||||||
|
log "xorg-dummy.service: ACTIVE"
|
||||||
|
elif systemctl is-active --quiet xorg-dummy@"$USER" 2>/dev/null; then
|
||||||
|
log "xorg-dummy@$USER.service: ACTIVE"
|
||||||
|
else
|
||||||
|
warn "xorg-dummy.service не найден в активных юнитах. Это может быть нормально, если драйвер загружен иначе."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем через xrandr у активного пользователя (display :1 для dummy)
|
||||||
|
if DISPLAY=:1 xrandr --listmonitors 2>/dev/null | grep -qi "DUMMY\|dummy"; then
|
||||||
|
log "xrandr: dummy монитор обнаружен (DISPLAY=:1)"
|
||||||
|
else
|
||||||
|
warn "xrandr не нашел DUMMY монитор на DISPLAY=:1. Попробуйте DISPLAY=:0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "--- Шаг 2: Статус локального клиента RustDesk --"
|
||||||
|
if systemctl is-active --quiet rustdesk-client-local 2>/dev/null; then
|
||||||
|
log "rustdesk-client-local.service: ACTIVE"
|
||||||
|
else
|
||||||
|
warn "rustdesk-client-local.service: NOT ACTIVE — будет создан/включён ниже"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# 2. Проверить/создать RustDesk.toml и защитить его
|
||||||
|
log "--- Шаг 3: Проверка/создание RustDesk.toml ---"
|
||||||
|
|
||||||
|
mkdir -p "$RUSTDESK_DIR"
|
||||||
|
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
log "RustDesk.toml найден: $CONFIG_FILE"
|
||||||
|
# Показать текущий ID
|
||||||
|
CURRENT_ID=$(rustdesk --get-id 2>/dev/null || echo "N/A")
|
||||||
|
log "Текущий RustDesk ID: $CURRENT_ID"
|
||||||
|
|
||||||
|
# Убедиться что в конфиге нужный сервер
|
||||||
|
if grep -q 'custom-rendezvous-server' "$CONFIG_FILE"; then
|
||||||
|
log "custom-rendezvous-server уже задан в конфиге"
|
||||||
|
else
|
||||||
|
log "custom-rendezvous-server НЕ задан — добавляем публичный сервер"
|
||||||
|
cat >> "$CONFIG_FILE" <<'TOML'
|
||||||
|
|
||||||
|
[options]
|
||||||
|
custom-rendezvous-server = "rs-ny.rustdesk.com:21116"
|
||||||
|
TOML
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
CURRENT_ID=$(rustdesk --get-id 2>/dev/null || echo "")
|
||||||
|
if [[ -z "$CURRENT_ID" ]]; then
|
||||||
|
warn "RustDesk.toml ОТСУТСТВУЕТ, и rustdesk --get-id не вернул ID."
|
||||||
|
warn "Возможно, RustDesk ещё ни разу не запускался с GUI (нужен xorg-dummy)."
|
||||||
|
warn "Если ID ещё не сгенерирован — создайте конфиг вручную:"
|
||||||
|
warn " mkdir -p $RUSTDESK_DIR && cat > $CONFIG_FILE <<'TOML'"
|
||||||
|
warn " [options]"
|
||||||
|
warn " custom-rendezvous-server = \"rs-ny.rustdesk.com:21116\""
|
||||||
|
warn " TOML"
|
||||||
|
warn "Затем перезапустите rustdesk-client-local и повторите скрипт."
|
||||||
|
else
|
||||||
|
log "ID получен ($CURRENT_ID), но RustDesk.toml отсутствует — создаём из текущих настроек"
|
||||||
|
cat > "$CONFIG_FILE" <<TOML
|
||||||
|
[options]
|
||||||
|
custom-rendezvous-server = "rs-ny.rustdesk.com:21116"
|
||||||
|
TOML
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Правильные права
|
||||||
|
chown -R "$USER:$USER" "$RUSTDESK_DIR"
|
||||||
|
chmod 600 "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# 3. Защита chattr +i (только root)
|
||||||
|
log "--- Шаг 4: Применение chattr +i к RustDesk.toml ---"
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
chattr +i "$CONFIG_FILE"
|
||||||
|
log "chattr +i применён к $CONFIG_FILE"
|
||||||
|
lsattr "$CONFIG_FILE" | tee -a "$LOG"
|
||||||
|
else
|
||||||
|
warn "Файл $CONFIG_FILE не существует — chattr +i НЕ применён!"
|
||||||
|
warn "Сначала убедитесь, что RustDesk сгенерировал конфигурацию."
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# 4. Создать/обновить write-rustdesk-config.sh (для экстренной перезаписи)
|
||||||
|
WRITE_SCRIPT="$SCRIPT_DIR/write-rustdesk-config.sh"
|
||||||
|
log "--- Шаг 5: Создание write-rustdesk-config.sh ---"
|
||||||
|
cat > "$WRITE_SCRIPT" <<'SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Этот скрипт временно снимает защиту, перезаписывает конфиг и ставит защиту обратно.
|
||||||
|
# Запускать от root только если понимаете зачем.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
USER="${1:-user}"
|
||||||
|
USER_HOME="/home/$USER"
|
||||||
|
CONFIG_FILE="$USER_HOME/.config/rustdesk/RustDesk.toml"
|
||||||
|
|
||||||
|
[[ $EUID -ne 0 ]] && { echo "Run as root"; exit 1; }
|
||||||
|
|
||||||
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||||
|
echo "Файл $CONFIG_FILE не найден. Нечего перезаписывать."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Снимаем immutable..."
|
||||||
|
chattr -i "$CONFIG_FILE"
|
||||||
|
|
||||||
|
echo "Перезаписываем RustDesk.toml..."
|
||||||
|
cat > "$CONFIG_FILE" <<'TOML'
|
||||||
|
[options]
|
||||||
|
custom-rendezvous-server = "rs-ny.rustdesk.com:21116"
|
||||||
|
TOML
|
||||||
|
|
||||||
|
chown "$USER:$USER" "$CONFIG_FILE"
|
||||||
|
chmod 600 "$CONFIG_FILE"
|
||||||
|
|
||||||
|
echo "Ставим immutable обратно..."
|
||||||
|
chattr +i "$CONFIG_FILE"
|
||||||
|
lsattr "$CONFIG_FILE"
|
||||||
|
echo "Готово."
|
||||||
|
SCRIPT
|
||||||
|
chmod +x "$WRITE_SCRIPT"
|
||||||
|
chown root:root "$WRITE_SCRIPT"
|
||||||
|
log "write-rustdesk-config.sh создан: $WRITE_SCRIPT"
|
||||||
|
|
||||||
|
|
||||||
|
# 5. Создать/обновить systemd-сервис rustdesk-client-local
|
||||||
|
SERVICE_FILE="/etc/systemd/system/rustdesk-client-local.service"
|
||||||
|
log "--- Шаг 6: Создание systemd unit rustdesk-client-local.service ---"
|
||||||
|
|
||||||
|
if [[ -f "$SERVICE_FILE" ]]; then
|
||||||
|
cp "$SERVICE_FILE" "$SERVICE_FILE.bak.$(date +%s)"
|
||||||
|
log "Backup создан: $SERVICE_FILE.bak.*"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$SERVICE_FILE" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=RustDesk Client Local (headless host)
|
||||||
|
After=network.target xorg-dummy.service
|
||||||
|
Wants=xorg-dummy.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$USER
|
||||||
|
Environment=DISPLAY=:1
|
||||||
|
Environment=XAUTHORITY=$USER_HOME/.Xauthority
|
||||||
|
WorkingDirectory=$USER_HOME
|
||||||
|
ExecStart=/usr/bin/rustdesk --tray
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StartLimitInterval=60
|
||||||
|
StartLimitBurst=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now rustdesk-client-local.service || true
|
||||||
|
log "rustdesk-client-local.service: enabled & restarted"
|
||||||
|
|
||||||
|
|
||||||
|
# 6. Текущий ID
|
||||||
|
log "--- Шаг 7: Проверка RustDesk ID хоста ---"
|
||||||
|
ID=$(rustdesk --get-id 2>/dev/null || echo "")
|
||||||
|
if [[ -n "$ID" ]]; then
|
||||||
|
log "ТЕКУЩИЙ RUSTDESK ID: $ID"
|
||||||
|
else
|
||||||
|
warn "Не удалось получить ID (rustdesk --get-id вернул пустоту)"
|
||||||
|
warn "Убедитесь, что rustdesk установлен и systemd-сервис запущён."
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
log "========== Результат =========="
|
||||||
|
log "RustDesk ID: ${ID:-пусто}"
|
||||||
|
log "Конфиг защищён chattr: $(lsattr "$CONFIG_FILE" 2>/dev/null || echo 'не удалось проверить')"
|
||||||
|
log "systemd unit: $(systemctl is-active rustdesk-client-local 2>/dev/null || echo 'неактивен')"
|
||||||
|
log "Записи в systemd journal: journalctl -u rustdesk-client-local -f"
|
||||||
|
log "Лог скрипта: $LOG"
|
||||||
|
log "Готово."
|
||||||
95
scripts/install-rustdesk-server.sh
Executable file
95
scripts/install-rustdesk-server.sh
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# RustDesk Server Native Installer (hbbs + hbbr)
|
||||||
|
# Debian 12 / x86_64 / static musl binaries from GitHub releases
|
||||||
|
# Idempotent
|
||||||
|
|
||||||
|
VERSION="1.1.15"
|
||||||
|
ARCH="amd64"
|
||||||
|
ZIP_URL="https://github.com/rustdesk/rustdesk-server/releases/download/${VERSION}/rustdesk-server-linux-${ARCH}.zip"
|
||||||
|
|
||||||
|
INSTALL_DIR="/usr/local/bin"
|
||||||
|
DATA_DIR="/var/lib/rustdesk-server"
|
||||||
|
CONFIG_DIR="/etc/rustdesk-server"
|
||||||
|
SERVICE_DIR="/etc/systemd/system"
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
log "Installing: $1"
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log "=== RustDesk Server Install ==="
|
||||||
|
log "Version: $VERSION"
|
||||||
|
log "Arch: $ARCH"
|
||||||
|
|
||||||
|
need_cmd curl
|
||||||
|
need_cmd unzip
|
||||||
|
|
||||||
|
log "Downloading release $VERSION..."
|
||||||
|
curl -fsSL -o "$TMP_DIR/rustdesk-server.zip" "$ZIP_URL"
|
||||||
|
|
||||||
|
log "Extracting..."
|
||||||
|
unzip -q -o "$TMP_DIR/rustdesk-server.zip" -d "$TMP_DIR"
|
||||||
|
|
||||||
|
log "Installing binaries to $INSTALL_DIR..."
|
||||||
|
install -m 755 "$TMP_DIR/$ARCH/hbbs" "$INSTALL_DIR/hbbs"
|
||||||
|
install -m 755 "$TMP_DIR/$ARCH/hbbr" "$INSTALL_DIR/hbbr"
|
||||||
|
install -m 755 "$TMP_DIR/$ARCH/rustdesk-utils" "$INSTALL_DIR/rustdesk-utils"
|
||||||
|
|
||||||
|
log "Creating directories..."
|
||||||
|
mkdir -p "$DATA_DIR" "$CONFIG_DIR"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
for service in rustdesk-hbbs.service rustdesk-hbbr.service; do
|
||||||
|
src="$SCRIPT_DIR/$service"
|
||||||
|
dst="$SERVICE_DIR/$service"
|
||||||
|
if [ -f "$src" ]; then
|
||||||
|
cp -f "$src" "$dst"
|
||||||
|
chmod 644 "$dst"
|
||||||
|
log "Installed: $dst"
|
||||||
|
else
|
||||||
|
log "WARNING: $src not found — ensure systemd unit files are in the same directory."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Reloading systemd..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
log "Enabling services..."
|
||||||
|
systemctl enable rustdesk-hbbs.service || true
|
||||||
|
systemctl enable rustdesk-hbbr.service || true
|
||||||
|
|
||||||
|
log "Starting services..."
|
||||||
|
systemctl restart rustdesk-hbbs.service || true
|
||||||
|
systemctl restart rustdesk-hbbr.service || true
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
log "=== Status ==="
|
||||||
|
systemctl status rustdesk-hbbs.service --no-pager || true
|
||||||
|
systemctl status rustdesk-hbbr.service --no-pager || true
|
||||||
|
|
||||||
|
PUB_KEY="$DATA_DIR/id_ed25519.pub"
|
||||||
|
if [ -f "$PUB_KEY" ]; then
|
||||||
|
log "=== Server Public Key ==="
|
||||||
|
cat "$PUB_KEY"
|
||||||
|
else
|
||||||
|
log "Public key not found yet. It will be generated on first successful hbbs start."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "=== Listening Ports ==="
|
||||||
|
ss -tlnp | grep -E '21115|21116|21117' || true
|
||||||
|
|
||||||
|
log "=== Done ==="
|
||||||
39
scripts/open-rustdesk-ports.sh
Executable file
39
scripts/open-rustdesk-ports.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v ufw >/dev/null 2>&1; then
|
||||||
|
log "UFW not installed. Installing..."
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq ufw
|
||||||
|
fi
|
||||||
|
|
||||||
|
UFW_STATUS=$(ufw status numbered 2>/dev/null || true)
|
||||||
|
|
||||||
|
open_port() {
|
||||||
|
local port=$1
|
||||||
|
local proto=${2:-tcp}
|
||||||
|
local rule="$port/$proto"
|
||||||
|
if echo "$UFW_STATUS" | grep -q "$rule"; then
|
||||||
|
log "Already open: $rule"
|
||||||
|
else
|
||||||
|
log "Opening: $rule"
|
||||||
|
ufw allow "$rule"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log "=== Opening RustDesk Server ports ==="
|
||||||
|
open_port 21115 tcp
|
||||||
|
open_port 21116 tcp
|
||||||
|
open_port 21116 udp
|
||||||
|
open_port 21117 tcp
|
||||||
|
open_port 21118 tcp
|
||||||
|
open_port 21119 tcp
|
||||||
|
|
||||||
|
log "=== UFW status ==="
|
||||||
|
ufw status verbose
|
||||||
|
|
||||||
|
log "=== Done ==="
|
||||||
17
scripts/rustdesk-client-local.service
Normal file
17
scripts/rustdesk-client-local.service
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RustDesk Client (local hbbs)
|
||||||
|
After=xorg-dummy.service network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Environment="DISPLAY=:0"
|
||||||
|
Environment="XAUTHORITY=/root/.Xauthority"
|
||||||
|
ExecStartPre=/root/write-rustdesk-config.sh
|
||||||
|
ExecStartPre=/bin/sleep 2
|
||||||
|
ExecStart=/usr/share/rustdesk/rustdesk --server
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
23
scripts/rustdesk-hbbr.service
Normal file
23
scripts/rustdesk-hbbr.service
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RustDesk Server (hbbr — relay)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/hbbr
|
||||||
|
WorkingDirectory=/var/lib/rustdesk-server
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/var/lib/rustdesk-server
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=rustdesk-hbbr
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
23
scripts/rustdesk-hbbs.service
Normal file
23
scripts/rustdesk-hbbs.service
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RustDesk Server (hbbs — ID / NAT relay)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/hbbs
|
||||||
|
WorkingDirectory=/var/lib/rustdesk-server
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/var/lib/rustdesk-server
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=rustdesk-hbbs
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
56
scripts/vps/start-rustdesk-xvfb.sh
Normal file
56
scripts/vps/start-rustdesk-xvfb.sh
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# RustDesk VPS - запуск с Xvfb + xcompmgr для корректного GUI рендеринга
|
||||||
|
|
||||||
|
export DISPLAY=:0
|
||||||
|
export XAUTHORITY=/root/.Xauthority
|
||||||
|
export LIBGL_ALWAYS_SOFTWARE=1
|
||||||
|
export LIBVA_DRIVER_NAME=none
|
||||||
|
export VDPAU_DRIVER=none
|
||||||
|
|
||||||
|
# 1. Очищаем старые сессии
|
||||||
|
pkill -f Xvfb
|
||||||
|
pkill -f rustdesk
|
||||||
|
sleep 1
|
||||||
|
rm -f /tmp/.X11-unix/X0 /tmp/.X0-lock 2>/dev/null
|
||||||
|
|
||||||
|
# 2. Запускаем Xvfb с COMPOSITE extension
|
||||||
|
echo "Starting Xvfb..."
|
||||||
|
/usr/bin/Xvfb :0 -screen 0 1920x1080x24 +extension GLX +extension RANDR +extension RENDER +extension COMPOSITE -ac &
|
||||||
|
echo "Xvfb PID: $!"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 3. Проверяем запуск Xvfb
|
||||||
|
if ! pgrep -f "Xvfb :0" >/dev/null; then
|
||||||
|
echo "ERROR: Xvfb failed to start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Xvfb OK"
|
||||||
|
|
||||||
|
# 4. Запускаем композитор
|
||||||
|
echo "Starting xcompmgr..."
|
||||||
|
if command -v xcompmgr >/dev/null 2>&1; then
|
||||||
|
/usr/bin/xcompmgr -d :0 &
|
||||||
|
else
|
||||||
|
echo "WARNING: xcompmgr not found. Install with: apt-get install -y xcompmgr"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Настраиваем xauth
|
||||||
|
xauth -f "$XAUTHORITY" add :0 . $(mcookie) 2>/dev/null || true
|
||||||
|
chmod 600 "$XAUTHORITY"
|
||||||
|
|
||||||
|
# 6. Пишем конфиг
|
||||||
|
/root/write-rustdesk-config.sh
|
||||||
|
|
||||||
|
# 7. Запускаем RustDesk
|
||||||
|
echo "Starting RustDesk..."
|
||||||
|
nohup /usr/share/rustdesk/rustdesk &
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
echo "=== Status ==="
|
||||||
|
echo "Xvfb: $(pgrep -a Xvfb | grep :0 || echo 'NOT RUNNING')"
|
||||||
|
echo "RustDesk:"
|
||||||
|
pgrep -a rustdesk || echo 'NOT RUNNING'
|
||||||
|
echo ""
|
||||||
|
echo "RustDesk ID: $(/usr/share/rustdesk/rustdesk --get-id 2>/dev/null || echo 'N/A')"
|
||||||
|
echo ""
|
||||||
|
echo "To connect to host: rustdesk <host-id>"
|
||||||
18
scripts/write-rustdesk-config.sh
Executable file
18
scripts/write-rustdesk-config.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
CONFIG="/root/.config/rustdesk/RustDesk2.toml"
|
||||||
|
|
||||||
|
printf "%s\n" \
|
||||||
|
"rendezvous_server = 'localhost:21116'" \
|
||||||
|
"custom-rendezvous-server = 'localhost:21116'" \
|
||||||
|
"key = 'JDSXd9sxN6Y7mIzu3krSpPTTc4yjChfqpzgIH5fl9Iw='" \
|
||||||
|
"nat_type = 1" \
|
||||||
|
"serial = 0" \
|
||||||
|
"unlock_pin = ''" \
|
||||||
|
"" \
|
||||||
|
"[options]" \
|
||||||
|
"local-ip-addr = '2.59.219.61'" \
|
||||||
|
> "$CONFIG"
|
||||||
|
|
||||||
|
chmod 600 "$CONFIG"
|
||||||
|
echo "Config written to $CONFIG"
|
||||||
Reference in New Issue
Block a user