Files
dmr-server/install.sh
T
viktorиClaude Opus 4.8 8e3b9144c3 DMR-сервер: мастер Homebrew на stdlib для Pi-Star
Ядро протокола форкнуто из dmrhubv2 (~1300 строк из 31700): analyzer, server,
parrot, dmr_voice, bptc. Отрезаны портал, БД, мосты, алерты, динамические
подписки. Своё: конфиг, состояние, маршрутизатор, панель, установщик.

Ноль зависимостей — на Debian 13 действует PEP 668, pip в систему запрещён.

Две правки под Pi-Star, которых в исходном коде не было:
 • peer_id переписывается на получателя (байты 11-14) через хук encode_for —
   так делают HBlink3/HBlink4 и BrandMeister, а DMRGateway на Pi-Star стоит
   между MMDVMHost и сетью;
 • приватный вызов при неизвестном абоненте уходит всем, а не в никуда
   (взято из HBlink4: user cache with broadcast fallback).

docs/PROTOCOL.md фиксирует, что тест-вектор auth-хеша в спецификации 2015 года
ОШИБОЧЕН (посчитан от ASCII-строки соли вместо четырёх байт). Правка кода по
нему сломала бы вход всем хотспотам.

tests/test_e2e.py: 15 проверок настоящими UDP-кадрами MMDVMHost. Все проходят.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvX94Vwy7vXWEuiFm1g9Av
2026-07-28 23:32:04 +09:00

137 строки
5.0 KiB
Bash

#!/usr/bin/env bash
# Установка DMR-сервера. Debian/Ubuntu, systemd. Зависимостей нет — только python3.
#
# sudo bash install.sh
#
# Повторный запуск безопасен: конфиг и пароли не перезаписываются.
set -euo pipefail
APP=dmr-server
DEST=/opt/$APP
CFGDIR=/etc/$APP
USER=dmrsrv
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[ "$(id -u)" = 0 ] || { echo "нужен root: sudo bash install.sh" >&2; exit 1; }
command -v python3 >/dev/null || {
echo "==> ставлю python3"
DEBIAN_FRONTEND=noninteractive apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3
}
PYV=$(python3 -c 'import sys;print("%d.%d"%sys.version_info[:2])')
python3 - <<'PY' || { echo "нужен Python 3.10+, найден $PYV" >&2; exit 1; }
import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)
PY
echo "==> python3 $PYV"
echo "==> служебный пользователь $USER"
id "$USER" >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin "$USER"
echo "==> код в $DEST"
install -d -m 755 "$DEST"
rm -rf "$DEST/dmrsrv"
cp -r "$SRC/dmrsrv" "$DEST/dmrsrv"
[ -d "$SRC/tests" ] && { rm -rf "$DEST/tests"; cp -r "$SRC/tests" "$DEST/tests"; }
find "$DEST" -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true
chown -R root:root "$DEST"
echo "==> конфигурация в $CFGDIR"
install -d -m 750 -o "$USER" -g "$USER" "$CFGDIR"
if [ -f "$CFGDIR/config.json" ]; then
echo " config.json уже есть — не трогаю"
else
# Пароль панели сервер сгенерирует сам при первом запуске и напишет в журнал.
cat > "$CFGDIR/config.json" <<'JSON'
{
"bind": "0.0.0.0",
"port": 62031,
"passphrase": "passw0rd",
"panel": { "bind": "0.0.0.0", "port": 8080, "user": "admin", "password": "" },
"groups": [
{ "tg": 9, "name": "Локальная", "slot": 2 },
{ "tg": 1, "name": "Общая", "slot": 1 }
],
"parrot": { "enabled": true, "tg": 9990, "delay": 2.0 },
"peers_allowlist": [],
"log_keep": 500
}
JSON
chown "$USER:$USER" "$CFGDIR/config.json"
chmod 600 "$CFGDIR/config.json"
echo " создан конфиг по умолчанию (ОБЯЗАТЕЛЬНО смени passphrase в панели)"
fi
echo "==> systemd"
cat > /etc/systemd/system/$APP.service <<EOF
[Unit]
Description=DMR-сервер (Homebrew/MMDVM мастер)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$USER
Group=$USER
Environment=DMRSRV_CONFIG=$CFGDIR/config.json
Environment=PYTHONUNBUFFERED=1
WorkingDirectory=$DEST
ExecStart=/usr/bin/python3 -m dmrsrv
Restart=on-failure
RestartSec=3
# Сервису нужны только свой конфиг и сеть — остальное закрываем.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$CFGDIR
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now $APP.service
sleep 2
echo
if systemctl is-active --quiet $APP; then
echo "==> служба работает"
else
echo "==> СЛУЖБА НЕ ПОДНЯЛАСЬ:"; journalctl -u $APP -n 20 --no-pager; exit 1
fi
PORT=$(python3 -c "import json;print(json.load(open('$CFGDIR/config.json'))['port'])")
PPORT=$(python3 -c "import json;print(json.load(open('$CFGDIR/config.json'))['panel']['port'])")
PPASS=$(python3 -c "import json;print(json.load(open('$CFGDIR/config.json'))['panel']['password'])" 2>/dev/null || true)
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
cat <<INFO
┌────────────────────────────────────────────────────────────
│ DMR-сервер запущен
├────────────────────────────────────────────────────────────
│ Хотспоты (Pi-Star): $IP порт UDP $PORT
│ Пароль хотспотов: см. "passphrase" в $CFGDIR/config.json
│
│ Панель: http://$IP:$PPORT
│ Пользователь: admin
│ Пароль панели: ${PPASS:-<сгенерирован, см. journalctl>}
└────────────────────────────────────────────────────────────
Журнал: journalctl -u $APP -f
Перезапуск: systemctl restart $APP
В Pi-Star: Configuration -> DMR Master -> DMRGateway/"DMR Master"
указать этот адрес и порт $PORT, пароль — passphrase из конфига.
INFO