# LPRA - LongPolling Remote Access
Лёгкий self-hosted сервис для доступа к Linux-машинам **без белого IP** (за NAT).
Агент на клиенте сам исходяще подключается к серверу - по WebSocket, а где он не проходит,
по HTTP long-polling. В веб-панели видно список машин, и по каждой есть терминал в браузере,
файловый менеджер, проброс портов и статус.
```
браузер ──HTTPS/WS──> [Cloudflare] ──> [nginx TLS] ──> [Go server 127.0.0.1:8091] <──WS / long-poll── агент (за NAT)
│ ▲
SQLite └──── прямой TCP/UDP-туннель :8446 (проброс tcp/udp)
```
## Возможности
- **Одна страница** - Машины, WorkSpace (терминалы и файлы), Настройки; переходы без перезагрузки.
Вкладки WorkSpace живут, пока открыта вкладка браузера: уход на другие страницы их не
закрывает, перезагрузка сбрасывает (браузер сначала спросит). «Порты», «Статус» и «Изменить»
машины открываются окном поверх текущей страницы - из списка машин или правым кликом
по заголовку вкладки или панели.
- **WorkSpace** - вкладки на нескольких машинах сразу, до четырёх панелей в одной вкладке -
терминал или файловый менеджер (перетаскиванием, с изменяемыми размерами), переименование
вкладок. На телефоне - панель клавиш (Esc, Ctrl, Alt, стрелки и т.д.).
- **SSH-терминал** - в меню «+»: агент открывает SSH-соединение к другому хосту (по паролю,
ключу - можно загрузить файл - или своими ключами из `~/.ssh`), шелл открывается во вкладке,
а шаги подключения видны в статусе. Ключ хоста запоминается при первом входе
(`ssh_known_hosts` рядом с `state.json` агента); если он сменится, вход откажет.
- **Файлы** - просмотр, редактор с подсветкой, hex-редактор, предпросмотр картинок, видео, PDF,
баз SQLite с правкой (значение по клику, добавление и удаление строк, вкладка SQL; агент меняет базу через сам SQLite, так что работающая база не ломается); загрузка и скачивание до 512 МБ кусками по 4 МБ (проходит через Cloudflare);
свойства (chmod/chown), zip/unzip.
- **Файлы по SSH** - в меню «+» › «Файловый менеджер» › «SSH-хост…»: машина подключается к хосту
и работает с файлами командами по SSH, SFTP не нужен. Есть всё, кроме редактора SQLite;
zip и распаковка - если на хосте установлены `zip` и `unzip`. Оборвалось подключение -
панель предложит переподключиться.
- **Перетаскивание файлов** - строку (или все отмеченные) в другую файловую панель или в папку
в ней: окно «Переместить / Копировать» у места броска, между машинами и SSH-хостами - с
прогрессом. В терминал той же машины или того же SSH-хоста - вставляется путь в кавычках,
в терминал другой - нельзя, с подсказкой почему. Задержать над заголовком вкладки - она
откроется. Только на компьютере.
- **Перемещение и копирование** - внутри машины или, кнопкой «сеть» в том же окне, **на другую
машину или на SSH-хост**: папка уходит потоком tar без промежуточных файлов, с прогрессом,
скоростью и отменой. «Переместить» удаляет исходник после успешной отправки. Правила для
существующего: сообщить / заменить / оставить как есть.
- **ИИ-ассистент** - круглая кнопка чата (на компьютере её и окно можно перетаскивать); каждый
включает себе сам в профиле. Видит вкладки, активный терминал или папку и список ваших машин,
отвечает потоком. Действия - кнопками: выполнить команду в терминале нужной машины, прочитать,
показать или записать файл - всё с правами пользователя. Плашка «Агент» - делает шаги сам,
спрашивая перед каждой командой и записью; «Думать» - сначала рассуждает (свёрнуто над
ответом). Модели задаёт администратор в «Настройки › ИИ-ассистент»: их может быть несколько, у каждой своё
название в чате (и короткое, до 12 символов, - для кнопки выбора), тип - Gradio (по умолчанию - прокси к Qwen Omni), OpenAI-совместимый API или Anthropic,
адрес, модель и ключ (хранится на сервере, в браузер не уходит). Если моделей несколько, в чате рядом с
плашками появляется выбор, первая - по умолчанию; выбор запоминается за пользователем. В «Аккаунт › Вид»
можно показывать выбор всегда.
Плашка «Поиск» даёт ассистенту искать в интернете; поисковик выбирается там же: DuckDuckGo
(по умолчанию, без настройки), свой SearXNG (нужен `search.formats: [json]`), Brave Search API
или Tavily (по ключу). Если выбранный не ответил, поиск идёт через DuckDuckGo.
- **Сохранённые SSH-хосты** - в окне SSH-подключения галочка «Сохранить»: хост и вход (пароль или
ключ) хранятся на сервере, у каждого пользователя свои, зашифрованы AES-256-GCM ключом сервера.
Ключ берётся из `LPRA_SECRET_KEY` (32 байта, hex или base64) или из файла `<база>.key` рядом с
базой, который создаётся сам - храните его вместе с бэкапом базы, без него сохранённые входы
не расшифровать.
- **Проброс портов** - `http` через панель, `tcp`/`udp` через прямой туннель.
- **Статус** - нагрузка, память, диски, сеть, тест скорости канала до сервера.
- **Вход** - пароль и/или ключи доступа (passkeys); пароль можно выключить, когда есть ключ.
- **PIN-код** (необязательный, 4 цифры) - спрашивается в каждой новой вкладке браузера, но не при
перезагрузке. На устройстве можно не спрашивать его 5/15/30/60 минут после ввода.
Пять неверных попыток - выход из сессии.
## Пользователи и роли
- **Администратор** видит всё: все машины, настройки, пользователей, ufw.
- **Пользователь** видит только выданные ему машины (терминал, файлы, порты, статус, «Изменить» без удаления);
настроек, других пользователей и чужих машин для него нет.
Роль и машины задаются в «Настройки → Пользователи» (квадратная кнопка у пользователя открывает список машин).
Там же галочка «Пароль» включает или выключает вход по паролю (выключить можно, когда у человека есть ключ доступа),
а «Закрепить» не даёт ему поменять это самому. Последнего администратора нельзя удалить или понизить.
## База данных
По умолчанию - SQLite (файл из `-db`, по умолчанию `lpra.db`). В «Настройки › База данных» можно выбрать
PostgreSQL: хост, порт, база, пользователь, пароль, SSL; кнопка «Проверить и сохранить» подключается и
записывает выбор в `<база>.backend.json` рядом с SQLite-файлом (права 0600), «Перезапустить сервер»
завершает процесс - systemd (`Restart=always`) поднимает его уже на PostgreSQL. При первом запуске на
пустой PostgreSQL все данные копируются из SQLite-файла, сам файл остаётся как был (к нему можно
вернуться тем же выбором). История ИИ-ассистента тоже хранится в базе - у каждого пользователя
последние 200 сообщений.
## Компоненты
- **Сервер** (`cmd/server`) - панель и точка встречи для агентов. Ставится за nginx (TLS).
- **Агент** (`cmd/agent`) - бинарь для Linux (amd64/arm64), systemd-сервис от root.
- **install.sh** - установщик агента, отдаётся сервером.
## Сборка
```sh
./build.sh
```
Получаются `bin/lpra-server*` и `dist/agent-linux-{amd64,arm64}`.
Версия задаётся в одном месте - `VERSION.txt`, она вшивается в сервер и агент.
## Установка сервера одной командой
На Linux-сервере (amd64 или arm64, systemd):
```sh
wget -qO- https://gitlab.com/itzredtea/lpra/-/raw/main/server-install.sh | sudo sh
```
Скрипт `server-install.sh`:
- качает бинарник сервера под архитектуру машины из [последнего релиза](https://gitlab.com/itzredtea/lpra/-/releases/permalink/latest)
и папку `dist/` (бинарники агентов) из ветки `main`, кладёт всё в `/opt/lpra`;
- ставит службу systemd `lpra-server` (порт спросит, по умолчанию `8091`);
- по желанию настраивает nginx: спросит домен, создаст сайт на порт 80 (при необходимости поставит nginx),
затем `certbot --nginx` - если certbot с плагином nginx не установлен, этот шаг пропускается;
- если включён ufw - предложит открыть 80, 443 и туннель `8446` tcp/udp;
- запускает службу, перезагружает nginx и проверяет, что сервер отвечает.
Вопросы задаются в терминале, даже когда скрипт идёт через `| sudo sh`. Без терминала ответы берутся
из переменных: `LPRA_PORT`, `LPRA_NGINX` (y/n), `LPRA_DOMAIN`, `LPRA_EMAIL`, `LPRA_CERTBOT` (y/n), `LPRA_UFW` (y/n):
```sh
wget -qO- https://gitlab.com/itzredtea/lpra/-/raw/main/server-install.sh | sudo LPRA_NGINX=y LPRA_DOMAIN=lpra.example.com sh
```
Повторный запуск той же командой - **обновление**: бинарник и `dist/` заменяются, база (`/opt/lpra/lpra.db`
и её ключ `lpra.db.key`), настройки службы и nginx остаются. После установки откройте `/setup` - мастер
создаст администратора.
## Запуск сервера
```sh
./bin/lpra-server -addr 127.0.0.1:8091 -dist ./dist -db ./lpra.db
```
| Флаг | Переменная | По умолчанию | |
|---|---|---|---|
| `-addr` | `LPRA_ADDR` | `127.0.0.1:8091` | адрес HTTP (за nginx) |
| `-db` | `LPRA_DB` | `lpra.db` | база SQLite |
| `-dist` | `LPRA_DIST` | `dist` | бинарники агента для `/download/` |
| `-tunnel` | `LPRA_TUNNEL_ADDR` | `:8446` | прямой туннель TCP+UDP (пусто - выключен) |
| `-insecure-cookies` | `LPRA_INSECURE_COOKIES=1` | | куки без Secure - только для локального запуска без TLS |
Пример юнита - `deploy/lpra-server.service`.
Первый запуск: откройте `/setup` - мастер создаст администратора. Дальше в `/settings`:
адрес хоста, enroll-секрет, короткий код установки и готовая команда.
### nginx
Конфиг - `deploy/nginx.conf.example`. Обязательно:
- заголовки `Upgrade` / `Connection` (через `map $http_upgrade $connection_upgrade`) - без них
WebSocket не пройдёт, и агенты и терминалы останутся на long-polling (работает, но медленнее
на больших выводах);
- `proxy_read_timeout` больше 25 с (удержание long-poll), `proxy_buffering off`.
### Прямой адрес (без Cloudflare)
В «Настройки → Адрес хоста» можно задать второй, прямой адрес - например `https://direct.lpra.example.com`,
DNS-запись без проксирования. Тогда:
- агенты, не достучавшись до основного адреса 30 секунд, переходят на прямой и каждые 5 минут проверяют,
не ожил ли основной; уже установленные агенты получают прямой адрес с сервера сами (он хранится в `state.json`);
- установщик берёт адреса с того домена, с которого его скачали: с основного - агент работает через основной
с резервом на прямой, с прямого - наоборот (и бинарник качается с прямого); если первый адрес не отвечает,
бинарник качается со второго. В «Полных вариантах» есть короткая команда и через прямой адрес;
- в «Изменить → Связь с сервером» у клиента, подключённого через прямой адрес, после статуса стоит «(direct)»;
- если хост туннеля не задан, агентам отдаётся хост прямого адреса;
- ключи доступа основного домена работают и на его поддомене.
### Cloudflare
Работает за проксирующим Cloudflare: WebSocket он пропускает, файлы идут кусками меньше лимита
на запрос, статика с хешем в имени кэшируется. Чтобы панель видела настоящие IP (ограничение
попыток входа и кода установки считается по адресу), включите в nginx real IP:
```nginx
real_ip_header CF-Connecting-IP;
set_real_ip_from 173.245.48.0/20; # и остальные диапазоны с https://www.cloudflare.com/ips/
```
Прямой туннель (`:8446`) - это не HTTP, через проксирующий Cloudflare он не идёт: в
«Настройки → Прямой туннель» укажите для агентов IP хоста или DNS-имя без проксирования.
## Установка агента
Короткой командой из `/settings` (код из 3-24 букв и цифр, кнопка «Новый код» меняет его):
```sh
curl -fsSL https://panel.example/i/<код> | sudo sh
```
Или полной:
```sh
curl -fsSL https://panel.example/install.sh | sudo HOST=https://panel.example ENROLL_SECRET=<секрет> sh
```
Без параметров установщик спросит секрет, а если агент уже стоит - предложит удалить.
Обновить агента можно кнопкой в панели («Изменить» → обновить).
## Связь агента с сервером
В «Изменить» → «Связь с сервером» для каждой машины:
- **auto** (по умолчанию) - WebSocket; если он не подключается, long-polling ровно минуту, потом
снова попытка WebSocket;
- **ws** - только WebSocket;
- **lp** - только long-polling.
## Проброс портов и прямой туннель
Правила создаются на странице машины «Порты». Режим `http` (в панели - http-lp) идёт через
соединение агента с панелью: по WebSocket, а если он недоступен - по long-polling;
`tcp`/`udp` - через прямой туннель: агент держит исходящее соединение к порту туннеля хоста,
по нему мультиплексируются все соединения.
Порт туннеля и хост для агентов меняются в `/settings` → «Прямой туннель»; агенты забирают их
сами, переустановка не нужна. Локальные `TUNNEL_ADDR` / `TUNNEL_PORT` в окружении агента имеют
приоритет (`TUNNEL_ADDR=off` отключает туннель).
Если на хосте активен ufw и сервер запущен от root, панель сама открывает порты правил и порт
туннеля, а при удалении правила закрывает. При `ProtectSystem=strict` в юните нужно
`ReadWritePaths=/etc/ufw /run`.
## Пользователь терминала
По умолчанию терминал открывается от пользователя агента (root). В «Изменить машину» можно выбрать
другого пользователя или создать нового (с паролем, по желанию с sudo) - если агент запущен от root.
Файловый менеджер работает от пользователя агента.
## Безопасность
- Пароли админов - bcrypt; сессии в HttpOnly/Secure/SameSite куках; CSRF на формах;
ограничение попыток входа, PIN и кода установки.
- Ключи доступа (WebAuthn) - вход без пароля; пароль можно выключить совсем.
- Агенты: общий enroll-секрет → индивидуальный токен (хранится хешем, сверка constant-time);
туннель авторизуется HMAC от токена.
- SSH-пароли и ключи передаются агенту один раз для подключения и нигде не сохраняются.
- Открытие терминала и SSH, перенос файлов на другие машины, смена пользователя пишутся в `audit`.
## Скриншоты