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
Этот коммит содержится в:
@@ -0,0 +1,136 @@
|
||||
#!/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
|
||||
Ссылка в новой задаче
Block a user