Ядро протокола форкнуто из 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
137 строки
5.0 KiB
Bash
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
|