outbound v0.6.4 [base]


Главный шлюз внешних подключений

README

# outbound

Шлюз внешних подключений: точки, потребители, привязки, health, журнал.

Тип: Модуль. Категория: `base`. Зависимости: pip-пакеты `urllib3`, `paho-mqtt`, `pysmb`, `pywinrm` (по видам точек); системные `openssh-client`, `sshpass`, `iputils-ping`, `nfs-common` (ssh/sftp, ping, NFS).

## Описание

Правило простое: ни один модуль не ходит наружу сам — все ходят через шлюз.

Шлюз оперирует тремя сущностями. **Точка** — это «куда подключаться»: вид, адрес и опции. **Потребитель** — модуль, который заявляет о своей потребности сам при старте. **Привязка** — связь «кто к какой точке». Вызов без привязки отклоняется с подсказкой, что привязать.

Секреты (пароли и ключи) живут только в `.env` модуля и подтягиваются по ссылке точки. В самих файлах точек секретов нет. Пароль можно ввести прямо в кабинете outbound: модуль сохранит его в `data/env/outbound.env` (0600) через `modsafe.env_set` и сразу применит, без перезапуска и без захода в modsafe. Отключение проверки TLS действует только для локальных адресов.

Точка умеет ходить через промежуточный хост и через прокси, есть сухой прогон без выполнения. Подключение проверяется пробным вызовом ещё до сохранения. Здоровье всех точек проверяется фоном по расписанию, а каждый вызов пишется в кольцевой журнал.

## Возможности

- `endpoint_list {}` → все точки подключения (без секретов; `ops` — какие операции умеет kind)
- `endpoint_add {name, kind, address, opts?, auth_ref?, password?, via?, proxy?, health?, note?}` → добавить точку (с пробным подключением до сохранения; `password` сразу сохраняется в `outbound.env`)
- `endpoint_update {name, patch: {...}, password?, probe?}` → частичное обновление; `password` — новый секрет, `probe=true` — проверить точку перед сохранением
- `endpoint_delete {name}` → удалить точку (привязки к ней сбрасываются)
- `endpoint_test {name}` → ручная проверка health сохранённой точки
- `endpoint_probe {name?, kind, address, password?, auth_ref?, opts?, via?, health?}` → проверить ещё не сохранённую точку (пароль не сохраняется)
- `consumer_list {}` → клиенты-потребители: `need_parts` (список нужд) и `bound_by_need` (какая точка на какую нужду)
- `consumer_ensure {module, need, desc}` → заявить потребность (первый вызов — статус pending)
- `bind {module, endpoint|null, endpoints?, needs?}` → привязать клиента к
  точке (null — отвязать), к нескольким (`endpoints[]`) или отдельно по
  нуждам (`needs: [{op, endpoint}, ...]`). Новый формат хранит
  `binding_needs`, поэтому `rounds` получает `http.request → home-mcp` и
  `fs.list → ssh-node` независимо от порядка; несовместимый kind
  отклоняется. Старый формат сохраняется для совместимости, но сбрасывает
  per-need карту.
- `unbind {module}` → снять привязку (возврат в pending)
- `call {consumer, op, params?, endpoint?}` → единый вызов наружу от имени
  модуля (требует привязки; при неоднозначности нескольких привязок
  `endpoint` обязателен)
- `health {}` → сводка health всех точек
- `log {limit?}` → кольцевой журнал вызовов (последние N)
- `mux_status {}` → пул ControlMaster прямых ssh-точек (мастера, hits/fallbacks)
- `kinds {}` → поддерживаемые kind точек
- `endpoint_base {consumer, field=base_url}` → адрес привязанной точки для потребителя (при нескольких привязках — ошибка: адрес неоднозначен)
- `fs.list {path}` → один каталог: `[{name, kind: dir|file|link, size, mtime}]`
- `fs.read {path, max_bytes?}` → текст файла (до 2 МБ): `{text, size, truncated}`
- `fs.grep {path, pattern, recursive?, max?}` → поиск на источнике: `{matches[{path,line,text}]}`
- `fs.read_b64 {path, max_bytes?}` → точные байты base64 (без обрезки
  пробелов/переводов строк; для правок)
- `fs.write_b64 {path, data}` → запись base64: tmp + `chmod/chown
  --reference` + `mv` (только `ssh`; остальные kind — явная ошибка)

### Файловый слой `fs.*`

Чтение для обходов/файловых сценариев — **на стороне источника**, без
монтирования; запись (`fs.write_b64`) — только для `ssh`:

- `ssh` — `find -printf` (один каталог), `head -c` (чтение), `grep -E`
  (поиск), `head | base64`/`base64 -d` (точные байты); нужен GNU
  find/grep/base64 (Linux);
- `sftp` — `ls -l` (разбор формата OpenSSH) и `get` во временный файл;
- `smb` — `listPath`/`retrieveFile` (pysmb), файл ≤4 МБ;
- `ftp` — `MLSD`/`RETR`;
- `nfs` — локальный каталог монтирования (только внутри `mount_path`).

`fs.grep` есть только у `ssh` и `nfs` (у остальных нет поиска на
источнике). Через веб (`/api/outbound/call`) `fs.*` запрещены: чтение
файлов привязанных хостов доступно только модулям через `outbound.call`.

## SSH-мультиплекс (ControlMaster)

Прямые `ssh`-точки (без `via`) работают через автопул — тот же приём, что
`sess` в туле `remote.sh`: первый вызов поднимает мастера (`-M -S <сокет>
-fN -o ControlPersist`), дальше команды идут через сокет без пароля
(`BatchMode=yes`). Выигрыш — как у сессий: повторный вызов ~0.05 с против
~0.3-3 с холодного (особенно заметно на Windows OpenSSH).
Отличия от `sess`: пул не ручной, а автоматический — поднятие при первом
`call`/`health`, idle-смерть по `ControlPersist` (по умолчанию 600 с),
мёртвый мастер определяется по `ssh -O check`, вызов при этом падает в
обычный холодный путь (в результате поле `mux`: `reuse`|`cold`|`fallback`).
`via` (jump-host) идёт мимо пула холодным путём. Отказ: `opts.ssh_mux=false`
на точке или `SSH_MUX=0` глобально. Сокеты эфемерные (`/tmp/obmux-UID`,
`700`), пароли в именах не хранятся; при `update`/`delete` точки мастер
сбрасывается, при `stop` модуля закрываются все.

Кодовая страница вывода: консольные утилиты Windows (sc, net) отвечают OEM
`cp866`, поэтому `drivers/base.decode_out` декодирует вывод как UTF-8, а при
ошибке — как `cp866` (иначе русские строки становятся `U+FFFD`-кашей). Для
нетипичной локали точки задаётся `opts.ssh_codepage` (например `cp1251`).

`known_hosts` хранится в `data/outbound/known_hosts` (0600): у службы
`botmod` может не быть `$HOME`, поэтому ssh больше не пытается создать
`/home/botmod/.ssh` при первом подключении к новой ноде.

## Кабинет: новая нода

Кнопка «➕ Добавить точку» открывает одно окно: имя, тип, host/порт,
пользователь, пароль, ключ или base_url. Для ssh по умолчанию выбран
`ssh` и порт `22`. Кнопка «Проверить» делает пробное подключение без
сохранения, «Добавить» — проверяет и сохраняет. Пароль пишется в
`data/env/outbound.env` под автоматическим `auth_ref` (`EP_<ИМЯ>`) и
сразу подхватывается; в modsafe заходить не нужно. У существующей точки
есть «✏️» — смена host/порта/пароля и повторная проверка. В списке
точка с сохранённым секретом/ключом помечена 🔑.

## Кабинет: клиенты и привязки

Клиенты сгруппированы по модулю. Если модуль заявил несколько нужд
(`rounds`: `http.request, fs.list`), под ним отдельная строка на каждую
нужду и свой выпадающий список точки. Это не даёт одной точкой случайно
заменить привязку второй нужды. Кнопка «Сохранить» пишет
`binding_needs` (op → точка) и общий список привязок; несовместимый
тип точки отклоняется сервером. Для модуля с одной нуждой остаётся
прежний выбор (в т.ч. несколько точек, если так было настроено).

## Права

- `outbound_manage` — Точки подключения и привязки клиентов (кабинет outbound)
- `outbound_view` — Просмотр точек, health и журнала outbound

## Web

- `GET /api/outbound/endpoints` — точки подключения
- `POST /api/outbound/endpoint` — добавить/обновить точку (пароль — `password`)
- `POST /api/outbound/endpoint_probe` — проверка до сохранения
- `POST /api/outbound/endpoint_test` — проверка сохранённой точки
- `GET /api/outbound/consumers` — потребители и привязки
- `POST /api/outbound/bind` — привязка клиента к точке
- `GET /api/outbound/health` — здоровье точек
- `GET /api/outbound/log` — журнал вызовов
- `POST /api/outbound/call` — отладочный вызов

## Конфигурация (`config.schema.json`)

- Опциональные: `HEALTH_INTERVAL` = `60`, `LOG_LIMIT` = `200`, `DEFAULT_TIMEOUT` = `15`, `SSH_MUX` = `1`, `SSH_MUX_TTL` = `600`

Манифест

{
  "name": "outbound",
  "version": "0.6.4",
  "category": "base",
  "description": "Шлюз внешних подключений: точки (ssh/http/tcp/udp/icmp/dns/wol/mqtt/ws/sftp/smb/ftp/nfs/winrm/docker/proxmox/telnet), клиенты-потребители, привязки, health, журнал. Режимы: via (jump-host), proxy, dry-run. Фронтенд — web/view.html+view.js (модульная вьюха).",
  "requires": [],
  "rights": [],
  "api_methods": [],
  "web_routes": [],
  "commit": "9fd145352f27a88375dc726aa21c06e767c2ec3d",
  "updated": "2026-10-21 19:14:40 +0300",
  "history": [
    {
      "version": "0.1.0",
      "date": "2026-09-11 09:24:04 +0300",
      "commit": "9743e4158b1b0f967484922446a8840ba15a065f"
    },
    {
      "version": "0.2.0",
      "date": "2026-09-11 10:01:30 +0300",
      "commit": "cf65c08b4cc6abfad21a04c1867f30480b026113"
    },
    {
      "version": "0.3.0",
      "date": "2026-09-11 11:58:15 +0300",
      "commit": "2d83b57f98c0fbb9c9b8a9a8a852cdeecd04772a"
    },
    {
      "version": "0.4.0",
      "date": "2026-09-11 12:41:31 +0300",
      "commit": "ace93ba4a466931a84cedce8e577ddcba817e4af"
    },
    {
      "version": "0.5.0",
      "date": "2026-09-11 14:04:42 +0300",
      "commit": "91a37aba1aa33b536f1942a5d43feaac7930b965"
    },
    {
      "version": "0.5.1",
      "date": "2026-09-12 22:57:42 +0300",
      "commit": "8f10c8697301f0eca9211e3f6a13ccd2bdd5c5fa"
    },
    {
      "version": "0.5.2",
      "date": "2026-09-12 23:22:16 +0300",
      "commit": "db9c47f797b3a96a29b646f86025a87d1eb4d645"
    },
    {
      "version": "0.5.3",
      "date": "2026-10-13 00:02:06 +0300",
      "commit": "085fc9b00158e340dd05810ee9ffb43f04224e17"
    },
    {
      "version": "0.6.0",
      "date": "2026-10-15 16:42:48 +0300",
      "commit": "4af49df380d9e7034b1db3afb21311d4ba55dfd0"
    },
    {
      "version": "0.6.1",
      "date": "2026-10-20 21:25:25 +0300",
      "commit": "c131021cd2d971a6bd17448976f6f303da28b10a"
    },
    {
      "version": "0.6.2",
      "date": "2026-10-21 15:12:30 +0300",
      "commit": "4cbf24d1a492e7762d2102d99c0cea79c7ce91f1"
    },
    {
      "version": "0.6.3",
      "date": "2026-10-21 19:01:05 +0300",
      "commit": "4a48c65d0406c22dbfc4e1ca76eaa5c77ef2eb81"
    },
    {
      "version": "0.6.4",
      "date": "2026-10-21 19:14:40 +0300",
      "commit": "9fd145352f27a88375dc726aa21c06e767c2ec3d"
    }
  ],
  "integrity": "sha256:5dba140cf44b26428ea7d6810dae386b6ef735cef65f60291354e65503abb13e",
  "files": "[18 файлов — см. вкладки ниже]",
  "note": "Главный шлюз внешних подключений",
  "wdesc": "Главный шлюз внешних подключений",
  "wbody": "",
  "_extra": {
    "uses_rights": "",
    "api": "",
    "events_emitted": "[]",
    "events_subscribed": "[]",
    "audit_allow": "",
    "shared_routes": "",
    "config_schema": "config.schema.json",
    "env_example": ".env.example"
  }
}

Файлы и исходники

Дерево файлов

  • · корень
    • 11.6 КБ
    • 0.2 КБ
    • 5.1 КБ
    • 33.4 КБ
    • 1.0 КБ
    • 2.5 КБ
    • 6.3 КБ
  • drivers/
    • 3.3 КБ
    • 4.3 КБ
    • 23.8 КБ
    • 13.1 КБ
    • 10.7 КБ
    • 8.5 КБ
    • 7.8 КБ
    • 13.4 КБ
    • 10.0 КБ
  • web/
    • 1.9 КБ
    • 20.6 КБ

Предпросмотр

Выберите файл в дереве выше — код откроется здесь.

История версий

versiondatecommit
0.6.42026-10-219fd145352f27
0.6.32026-10-214a48c65d0406
0.6.22026-10-214cbf24d1a492
0.6.12026-10-20c131021cd2d9
0.6.02026-10-154af49df380d9
0.5.32026-10-13085fc9b00158
0.5.22026-09-12db9c47f797b3
0.5.12026-09-128f10c8697301
0.5.02026-09-1191a37aba1aa3
0.4.02026-09-11ace93ba4a466
0.3.02026-09-112d83b57f98c0
0.2.02026-09-11cf65c08b4cc6
0.1.02026-09-119743e4158b1b