Add SSH monitoring server installer #3

Open
ochenstarik-ui wants to merge 15 commits from agent/server-monitor-installer into main
2 changed files with 553 additions and 13 deletions
Showing only changes of commit d50d28369a - Show all commits

View file

@ -13,6 +13,7 @@
| `ochenstarik-server-vpn-5.sh` | Устанавливает Xray и направляет системный трафик через VLESS + REALITY | Да; зависимости устанавливаются автоматически |
| `ochenstarik-server-panel-warp-6.sh` | Устанавливает 3x-ui и Cloudflare WARP, настраивает и проверяет их порты | Ubuntu 22.04/24.04/26.04 или Debian 12/13 |
| `ochenstarik-server-backup-7.sh` | Создаёт неизменяемый первичный снимок и настраивает выбранные расписания резервного копирования | Да; зависимости устанавливаются автоматически |
| `ochenstarik-server-monitor-manager.sh` | Устанавливает главный WireGuard Hub или подключаемый узел Server Monitor Manager | Да; Hub нужен белый IP, вторичному узлу — только исходящий доступ |
Файлы `*.env.example` являются только примерами. Не записывайте настоящие пароли, токены и приватные SSH-ключи в Git.
@ -65,6 +66,53 @@ chmod 700 ./*.sh
bash -n ./ИМЯ-СКРИПТА.sh
```
## Server Monitor Manager: своя сеть без Tailscale
Файл `ochenstarik-server-monitor-manager.sh` устанавливает лёгкую сеть WireGuard со звездной топологией. Главный сервер с белым IP становится Hub, а домашний и другие серверы подключаются к нему исходящими соединениями. Белый IP на вторичных узлах не нужен.
На главном сервере откройте входящий UDP-порт WireGuard (по умолчанию `51820`) в панели хостинга и запустите:
```bash
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/agent/server-monitor-installer/ochenstarik-server-monitor-manager.sh
chmod 700 ochenstarik-server-monitor-manager.sh
bash -n ochenstarik-server-monitor-manager.sh
sudo ./ochenstarik-server-monitor-manager.sh hub
```
После установки создайте отдельный секретный код конфигурации для каждого узла:
```bash
sudo ochenstarik-smm node-code hermes
sudo ochenstarik-smm node-code home
sudo ochenstarik-smm node-code server2
```
На соответствующем вторичном сервере скачайте тот же файл и выполните:
```bash
sudo ./ochenstarik-server-monitor-manager.sh node
```
Установщик попросит публичный SSH-ключ приложения и код, созданный для этого узла. Код содержит приватный ключ WireGuard узла: передавайте его только в защищённый терминал, используйте для одного сервера и создайте новый узел, если код стал известен посторонним.
Связи направленные и управляются на Hub. Например, разрешить Hermes доступ к домашнему серверу и ко второму серверу, а затем отключить только второй маршрут:
```bash
sudo ochenstarik-smm link-connect hermes home
sudo ochenstarik-smm link-connect hermes server2
sudo ochenstarik-smm link-disconnect hermes server2
```
Проверка состояния:
```bash
sudo ochenstarik-smm nodes
sudo ochenstarik-smm links
sudo ochenstarik-smm status
```
Hub маршрутизирует только явно разрешённые пары `источник → цель`; обратное направление включается отдельно. Вторичные серверы не соединяются друг с другом напрямую и не требуют Tailscale или другого внешнего сервиса.
## 1. Часовой пояс и настройка swap
Файл: `ochenstarik-server-1.sh`.

View file

@ -6,6 +6,18 @@ readonly MONITOR_USER="ochenstarik-monitor"
readonly MONITOR_HOME="/var/lib/${APP_NAME}"
readonly MONITOR_COMMAND="/usr/local/libexec/ochenstarik-server-monitor"
readonly AUTHORIZED_KEYS="${MONITOR_HOME}/.ssh/authorized_keys"
readonly MESH_DIR="/etc/${APP_NAME}"
readonly NODES_DIR="${MESH_DIR}/nodes"
readonly HUB_CONFIG="${MESH_DIR}/hub.conf"
readonly HUB_PRIVATE_KEY="${MESH_DIR}/hub.key"
readonly LINKS_FILE="${MESH_DIR}/links"
readonly HUB_HELPER="/usr/local/libexec/ochenstarik-smm-hub"
readonly HUB_CLI="/usr/local/sbin/ochenstarik-smm"
readonly WG_INTERFACE="smm0"
readonly WG_CONFIG="/etc/wireguard/${WG_INTERFACE}.conf"
readonly MESH_CIDR="10.77.0.0/24"
readonly HUB_ADDRESS="10.77.0.1"
readonly DEFAULT_WG_PORT="51820"
log() { printf '[+] %s\n' "$*"; }
warn() { printf '[!] %s\n' "$*" >&2; }
@ -43,13 +55,19 @@ read_public_key() {
|| die "Ожидается публичный ключ формата ssh-ed25519 AAAA..."
}
install_dependencies() {
install_monitor_dependencies() {
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y \
ca-certificates openssh-server openssh-client coreutils gawk
systemctl enable --now ssh
}
install_mesh_dependencies() {
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y \
wireguard-tools nftables iproute2 jq openssl
}
verify_public_key() {
local key_file
key_file="$(mktemp)"
@ -70,6 +88,43 @@ create_monitor_command() {
set -Eeuo pipefail
export LC_ALL=C
readonly HUB_HELPER="/usr/local/libexec/ochenstarik-smm-hub"
original_command="${SSH_ORIGINAL_COMMAND:-metrics}"
case "$original_command" in
""|metrics) ;;
"mesh nodes")
[[ -x "$HUB_HELPER" ]] || { echo "Mesh Hub не установлен" >&2; exit 1; }
exec sudo -n "$HUB_HELPER" nodes
;;
"mesh links")
[[ -x "$HUB_HELPER" ]] || { echo "Mesh Hub не установлен" >&2; exit 1; }
exec sudo -n "$HUB_HELPER" links
;;
"mesh status")
[[ -x "$HUB_HELPER" ]] || { echo "Mesh Hub не установлен" >&2; exit 1; }
exec sudo -n "$HUB_HELPER" status
;;
"mesh connect "*)
[[ -x "$HUB_HELPER" ]] || { echo "Mesh Hub не установлен" >&2; exit 1; }
read -r prefix action source target extra <<< "$original_command"
[[ "$prefix" == mesh && "$action" == connect && -n "$source" && -n "$target" && -z "${extra:-}" ]] \
|| { echo "Некорректная команда" >&2; exit 2; }
exec sudo -n "$HUB_HELPER" link-connect "$source" "$target"
;;
"mesh disconnect "*)
[[ -x "$HUB_HELPER" ]] || { echo "Mesh Hub не установлен" >&2; exit 1; }
read -r prefix action source target extra <<< "$original_command"
[[ "$prefix" == mesh && "$action" == disconnect && -n "$source" && -n "$target" && -z "${extra:-}" ]] \
|| { echo "Некорректная команда" >&2; exit 2; }
exec sudo -n "$HUB_HELPER" link-disconnect "$source" "$target"
;;
*)
echo "Разрешены только metrics и команды mesh" >&2
exit 2
;;
esac
read_mem_value() {
awk -v key="$1" '$1 == key ":" { print $2; exit }' /proc/meminfo
}
@ -117,14 +172,454 @@ verify_sshd() {
|| die "В sshd отключена аутентификация по публичному ключу"
}
install_monitoring() {
read_public_key
install_monitor_dependencies
verify_public_key
create_monitor_command
create_monitor_user
verify_sshd
systemctl reload ssh
}
create_hub_helper() {
install -d -m 0755 -o root -g root "$(dirname "$HUB_HELPER")"
cat > "$HUB_HELPER" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
export LC_ALL=C
readonly STATE_DIR="/etc/ochenstarik-server-monitor-manager"
readonly NODES_DIR="${STATE_DIR}/nodes"
readonly HUB_CONFIG="${STATE_DIR}/hub.conf"
readonly HUB_PRIVATE_KEY="${STATE_DIR}/hub.key"
readonly LINKS_FILE="${STATE_DIR}/links"
readonly WG_INTERFACE="smm0"
readonly WG_CONFIG="/etc/wireguard/${WG_INTERFACE}.conf"
readonly NFT_TABLE="ochenstarik_smm"
die() { printf '[x] %s\n' "$*" >&2; exit 1; }
log() { printf '[+] %s\n' "$*"; }
require_root() { [[ "$EUID" -eq 0 ]] || die "Требуются права root"; }
valid_name() { [[ "$1" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]]; }
config_value() {
local key="$1" file="$2"
awk -F= -v key="$key" '$1 == key { sub(/^[^=]*=/, ""); print; exit }' "$file"
}
node_file() { printf '%s/%s.node' "$NODES_DIR" "$1"; }
node_value() { config_value "$2" "$(node_file "$1")"; }
node_exists() { [[ -f "$(node_file "$1")" ]]; }
render_wireguard_config() {
local tmp name file public_key address
tmp="$(mktemp)"
{
printf '[Interface]\n'
printf 'Address = %s/24\n' "$(config_value HUB_ADDRESS "$HUB_CONFIG")"
printf 'ListenPort = %s\n' "$(config_value WG_PORT "$HUB_CONFIG")"
printf 'PrivateKey = %s\n' "$(<"$HUB_PRIVATE_KEY")"
for file in "$NODES_DIR"/*.node; do
[[ -e "$file" ]] || continue
name="$(config_value NAME "$file")"
public_key="$(config_value PUBLIC_KEY "$file")"
address="$(config_value ADDRESS "$file")"
printf '\n# SMM-NODE %s\n' "$name"
printf '[Peer]\nPublicKey = %s\nAllowedIPs = %s/32\n' "$public_key" "$address"
done
} > "$tmp"
install -m 0600 -o root -g root "$tmp" "$WG_CONFIG"
rm -f -- "$tmp"
if ip link show "$WG_INTERFACE" >/dev/null 2>&1; then
wg syncconf "$WG_INTERFACE" <(wg-quick strip "$WG_INTERFACE")
fi
}
next_address() {
local host address file used
for host in $(seq 2 254); do
address="10.77.0.${host}"
used=false
for file in "$NODES_DIR"/*.node; do
[[ -e "$file" ]] || continue
if [[ "$(config_value ADDRESS "$file")" == "$address" ]]; then
used=true
break
fi
done
[[ "$used" == true ]] || { printf '%s' "$address"; return 0; }
done
die "В подсети 10.77.0.0/24 закончились адреса"
}
base64url_encode() {
base64 -w0 | tr '+/' '-_' | tr -d '='
}
node_code() {
local name="$1" private_key public_key address endpoint port payload code file
valid_name "$name" || die "Имя: строчные латинские буквы, цифры и дефис, максимум 32 символа"
node_exists "$name" && die "Узел $name уже существует"
private_key="$(wg genkey)"
public_key="$(printf '%s' "$private_key" | wg pubkey)"
address="$(next_address)"
endpoint="$(config_value HUB_ENDPOINT "$HUB_CONFIG")"
port="$(config_value WG_PORT "$HUB_CONFIG")"
file="$(node_file "$name")"
umask 077
{
printf 'NAME=%s\n' "$name"
printf 'ADDRESS=%s\n' "$address"
printf 'PUBLIC_KEY=%s\n' "$public_key"
} > "$file"
render_wireguard_config
payload="$(printf 'VERSION=1\nNAME=%s\nADDRESS=%s/32\nPRIVATE_KEY=%s\nHUB_PUBLIC_KEY=%s\nENDPOINT=%s:%s\nALLOWED_IPS=10.77.0.0/24\n' \
"$name" "$address" "$private_key" "$(wg pubkey < "$HUB_PRIVATE_KEY")" "$endpoint" "$port")"
code="SMM1-$(printf '%s' "$payload" | base64url_encode)"
unset private_key payload
printf '\nСекретный код конфигурации для узла %s:\n%s\n\n' "$name" "$code"
printf 'Код содержит приватный ключ узла. Используйте его только на одном целевом сервере и не сохраняйте в чатах или логах.\n'
}
restore_firewall() {
local tmp source target source_ip target_ip first=true
tmp="$(mktemp)"
nft delete table inet "$NFT_TABLE" >/dev/null 2>&1 || true
{
printf 'table inet %s {\n' "$NFT_TABLE"
printf ' set links {\n type ipv4_addr . ipv4_addr\n'
if [[ -s "$LINKS_FILE" ]]; then
printf ' elements = { '
while read -r source target; do
[[ -n "$source" && -n "$target" ]] || continue
node_exists "$source" && node_exists "$target" || continue
source_ip="$(node_value "$source" ADDRESS)"
target_ip="$(node_value "$target" ADDRESS)"
[[ "$first" == true ]] || printf ', '
printf '%s . %s' "$source_ip" "$target_ip"
first=false
done < "$LINKS_FILE"
printf ' }\n'
fi
printf ' }\n'
printf ' chain forward {\n'
printf ' type filter hook forward priority 10; policy accept;\n'
printf ' iifname "%s" oifname "%s" ct state established,related accept\n' "$WG_INTERFACE" "$WG_INTERFACE"
printf ' iifname "%s" oifname "%s" ip saddr . ip daddr @links accept\n' "$WG_INTERFACE" "$WG_INTERFACE"
printf ' iifname "%s" oifname "%s" drop\n' "$WG_INTERFACE" "$WG_INTERFACE"
printf ' }\n}\n'
} > "$tmp"
nft -f "$tmp"
rm -f -- "$tmp"
}
link_connect() {
local source="$1" target="$2" tmp
valid_name "$source" && valid_name "$target" || die "Некорректное имя узла"
[[ "$source" != "$target" ]] || die "Источник и назначение совпадают"
node_exists "$source" || die "Узел $source не найден"
node_exists "$target" || die "Узел $target не найден"
if grep -Fxq "$source $target" "$LINKS_FILE" 2>/dev/null; then
log "Связь $source$target уже включена"
return
fi
tmp="$(mktemp)"
{ [[ ! -f "$LINKS_FILE" ]] || cat "$LINKS_FILE"; printf '%s %s\n' "$source" "$target"; } \
| sort -u > "$tmp"
install -m 0600 -o root -g root "$tmp" "$LINKS_FILE"
rm -f -- "$tmp"
restore_firewall
log "Связь $source$target включена"
}
link_disconnect() {
local source="$1" target="$2" tmp
valid_name "$source" && valid_name "$target" || die "Некорректное имя узла"
tmp="$(mktemp)"
if [[ -f "$LINKS_FILE" ]]; then
awk -v source="$source" -v target="$target" '!($1 == source && $2 == target)' "$LINKS_FILE" > "$tmp"
fi
install -m 0600 -o root -g root "$tmp" "$LINKS_FILE"
rm -f -- "$tmp"
restore_firewall
log "Связь $source$target отключена"
}
remove_node() {
local name="$1" public_key tmp
valid_name "$name" || die "Некорректное имя узла"
node_exists "$name" || die "Узел $name не найден"
public_key="$(node_value "$name" PUBLIC_KEY)"
wg set "$WG_INTERFACE" peer "$public_key" remove 2>/dev/null || true
rm -f -- "$(node_file "$name")"
tmp="$(mktemp)"
if [[ -f "$LINKS_FILE" ]]; then
awk -v name="$name" '$1 != name && $2 != name' "$LINKS_FILE" > "$tmp"
fi
install -m 0600 -o root -g root "$tmp" "$LINKS_FILE"
rm -f -- "$tmp"
render_wireguard_config
restore_firewall
log "Узел $name удалён"
}
list_nodes() {
local file name address public_key handshake now state age
now="$(date +%s)"
for file in "$NODES_DIR"/*.node; do
[[ -e "$file" ]] || continue
name="$(config_value NAME "$file")"
address="$(config_value ADDRESS "$file")"
public_key="$(config_value PUBLIC_KEY "$file")"
handshake="$(wg show "$WG_INTERFACE" latest-handshakes 2>/dev/null | awk -v key="$public_key" '$1 == key { print $2; exit }')"
handshake="${handshake:-0}"
state=offline
age=-1
if [[ "$handshake" =~ ^[0-9]+$ && "$handshake" -gt 0 ]]; then
age=$((now - handshake))
(( age <= 180 )) && state=online
fi
printf 'NODE=%s|%s|%s|%s\n' "$name" "$address" "$state" "$age"
done
}
list_links() {
local source target
[[ -f "$LINKS_FILE" ]] || return 0
while read -r source target; do
[[ -n "$source" && -n "$target" ]] || continue
printf 'LINK=%s|%s|enabled\n' "$source" "$target"
done < "$LINKS_FILE"
}
status() {
printf 'ROLE=hub\nINTERFACE=%s\nADDRESS=%s\nENDPOINT=%s:%s\n' \
"$WG_INTERFACE" \
"$(config_value HUB_ADDRESS "$HUB_CONFIG")" \
"$(config_value HUB_ENDPOINT "$HUB_CONFIG")" \
"$(config_value WG_PORT "$HUB_CONFIG")"
list_nodes
list_links
}
main() {
local action="${1:-status}"
require_root
[[ -r "$HUB_CONFIG" ]] || die "Mesh Hub не установлен"
case "$action" in
node-code) [[ $# -eq 2 ]] || die "Использование: $0 node-code NAME"; node_code "$2" ;;
node-remove) [[ $# -eq 2 ]] || die "Использование: $0 node-remove NAME"; remove_node "$2" ;;
nodes) list_nodes ;;
links) list_links ;;
link-connect) [[ $# -eq 3 ]] || die "Использование: $0 link-connect SOURCE TARGET"; link_connect "$2" "$3" ;;
link-disconnect) [[ $# -eq 3 ]] || die "Использование: $0 link-disconnect SOURCE TARGET"; link_disconnect "$2" "$3" ;;
render) render_wireguard_config ;;
firewall-restore) restore_firewall ;;
status) status ;;
*) die "Команды: node-code, node-remove, nodes, links, link-connect, link-disconnect, status" ;;
esac
}
main "$@"
EOF
chown root:root "$HUB_HELPER"
chmod 0750 "$HUB_HELPER"
ln -sfn "$HUB_HELPER" "$HUB_CLI"
}
configure_hub_firewall_service() {
cat > /etc/systemd/system/ochenstarik-smm-firewall.service <<EOF
[Unit]
Description=Ochenstarik Server Monitor mesh ACL
After=wg-quick@${WG_INTERFACE}.service
Wants=wg-quick@${WG_INTERFACE}.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=${HUB_HELPER} firewall-restore
[Install]
WantedBy=multi-user.target
EOF
chmod 0644 /etc/systemd/system/ochenstarik-smm-firewall.service
systemctl daemon-reload
systemctl enable --now ochenstarik-smm-firewall.service
}
configure_hub_sudo() {
cat > /etc/sudoers.d/ochenstarik-smm-hub <<EOF
${MONITOR_USER} ALL=(root) NOPASSWD: ${HUB_HELPER} *
EOF
chmod 0440 /etc/sudoers.d/ochenstarik-smm-hub
visudo -cf /etc/sudoers.d/ochenstarik-smm-hub >/dev/null \
|| die "Некорректный sudoers-файл Mesh Hub"
}
read_hub_endpoint() {
local endpoint port
if [[ -n "${SMM_HUB_ENDPOINT:-}" ]]; then
endpoint="$SMM_HUB_ENDPOINT"
elif [[ -r /dev/tty ]]; then
IFS= read -r -p 'Публичный IPv4 или домен главного сервера: ' endpoint < /dev/tty
else
die "Передайте адрес через SMM_HUB_ENDPOINT"
fi
endpoint="${endpoint//$'\r'/}"
[[ "$endpoint" =~ ^[A-Za-z0-9.-]+$ ]] || die "Некорректный IPv4 или домен"
if [[ -n "${SMM_WG_PORT:-}" ]]; then
port="$SMM_WG_PORT"
elif [[ -r /dev/tty ]]; then
IFS= read -r -p "UDP-порт WireGuard [${DEFAULT_WG_PORT}]: " port < /dev/tty
port="${port:-$DEFAULT_WG_PORT}"
else
port="$DEFAULT_WG_PORT"
fi
[[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) \
|| die "Порт должен быть от 1 до 65535"
HUB_ENDPOINT_VALUE="$endpoint"
WG_PORT_VALUE="$port"
}
install_hub() {
install_monitoring
install_mesh_dependencies
read_hub_endpoint
install -d -m 0700 -o root -g root "$MESH_DIR" "$NODES_DIR"
install -d -m 0700 -o root -g root /etc/wireguard
[[ -f "$HUB_PRIVATE_KEY" ]] || { umask 077; wg genkey > "$HUB_PRIVATE_KEY"; }
chmod 0600 "$HUB_PRIVATE_KEY"
{
printf 'HUB_ENDPOINT=%s\n' "$HUB_ENDPOINT_VALUE"
printf 'WG_PORT=%s\n' "$WG_PORT_VALUE"
printf 'HUB_ADDRESS=%s\n' "$HUB_ADDRESS"
printf 'MESH_CIDR=%s\n' "$MESH_CIDR"
} > "$HUB_CONFIG"
chmod 0600 "$HUB_CONFIG"
touch "$LINKS_FILE"
chmod 0600 "$LINKS_FILE"
create_hub_helper
"$HUB_HELPER" render
"$HUB_HELPER" firewall-restore
cat > /etc/sysctl.d/90-ochenstarik-smm-forward.conf <<'EOF'
net.ipv4.ip_forward = 1
EOF
chmod 0644 /etc/sysctl.d/90-ochenstarik-smm-forward.conf
sysctl -p /etc/sysctl.d/90-ochenstarik-smm-forward.conf >/dev/null
"$HUB_HELPER" firewall-restore
systemctl enable --now "wg-quick@${WG_INTERFACE}.service"
configure_hub_firewall_service
configure_hub_sudo
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q '^Status: active'; then
ufw allow "$WG_PORT_VALUE/udp" comment 'Server Monitor Mesh' >/dev/null
ufw route allow in on "$WG_INTERFACE" out on "$WG_INTERFACE" comment 'Server Monitor Mesh routed' >/dev/null
fi
log "Главный Mesh Hub установлен: ${HUB_ENDPOINT_VALUE}:${WG_PORT_VALUE}/udp"
log "Создать код узла: sudo ochenstarik-smm node-code hermes"
log "Включить связь: sudo ochenstarik-smm link-connect hermes home"
}
base64url_decode() {
local data="$1" remainder
data="${data//-/+}"
data="${data//_/\/}"
remainder=$(( ${#data} % 4 ))
if (( remainder == 2 )); then data+='=='
elif (( remainder == 3 )); then data+='='
elif (( remainder == 1 )); then return 1
fi
printf '%s' "$data" | base64 -d
}
payload_value() {
local payload="$1" key="$2"
printf '%s\n' "$payload" | awk -F= -v key="$key" '$1 == key { sub(/^[^=]*=/, ""); print; exit }'
}
read_join_code() {
if [[ -n "${SMM_JOIN_CODE:-}" ]]; then
JOIN_CODE="$SMM_JOIN_CODE"
elif [[ -r /dev/tty ]]; then
printf '\nНа главном сервере выполните: sudo ochenstarik-smm node-code ИМЯ\n'
IFS= read -r -s -p 'Вставьте секретный код SMM1: ' JOIN_CODE < /dev/tty
printf '\n'
else
die "Передайте код через SMM_JOIN_CODE"
fi
JOIN_CODE="${JOIN_CODE//$'\r'/}"
[[ "$JOIN_CODE" == SMM1-* ]] || die "Ожидается код формата SMM1-..."
}
install_node_mesh() {
local payload version name address private_key hub_public_key endpoint allowed_ips derived_public tmp ssh_port
install_mesh_dependencies
read_join_code
payload="$(base64url_decode "${JOIN_CODE#SMM1-}")" || die "Не удалось декодировать код"
version="$(payload_value "$payload" VERSION)"
name="$(payload_value "$payload" NAME)"
address="$(payload_value "$payload" ADDRESS)"
private_key="$(payload_value "$payload" PRIVATE_KEY)"
hub_public_key="$(payload_value "$payload" HUB_PUBLIC_KEY)"
endpoint="$(payload_value "$payload" ENDPOINT)"
allowed_ips="$(payload_value "$payload" ALLOWED_IPS)"
[[ "$version" == 1 ]] || die "Неподдерживаемая версия кода"
[[ "$name" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]] || die "Некорректное имя узла"
[[ "$address" =~ ^10\.77\.0\.([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])/32$ ]] \
|| die "Некорректный внутренний адрес"
[[ "$endpoint" =~ ^[A-Za-z0-9.-]+:[0-9]{1,5}$ ]] || die "Некорректный endpoint"
[[ "$allowed_ips" == "$MESH_CIDR" ]] || die "Некорректная mesh-подсеть"
derived_public="$(printf '%s' "$private_key" | wg pubkey 2>/dev/null)" \
|| die "Некорректный приватный WireGuard-ключ"
[[ "$(printf '%s' "$hub_public_key" | base64 -d 2>/dev/null | wc -c)" -eq 32 ]] \
|| die "Некорректный публичный ключ Hub"
install -d -m 0700 -o root -g root /etc/wireguard "$MESH_DIR"
tmp="$(mktemp)"
{
printf '[Interface]\nAddress = %s\nPrivateKey = %s\n' "$address" "$private_key"
printf '\n[Peer]\nPublicKey = %s\nEndpoint = %s\n' "$hub_public_key" "$endpoint"
printf 'AllowedIPs = %s\nPersistentKeepalive = 25\n' "$allowed_ips"
} > "$tmp"
install -m 0600 -o root -g root "$tmp" "$WG_CONFIG"
rm -f -- "$tmp"
printf 'ROLE=node\nNAME=%s\nADDRESS=%s\nPUBLIC_KEY=%s\n' \
"$name" "$address" "$derived_public" > "$MESH_DIR/node.conf"
chmod 0600 "$MESH_DIR/node.conf"
unset private_key payload JOIN_CODE SMM_JOIN_CODE
systemctl enable --now "wg-quick@${WG_INTERFACE}.service"
ssh_port="$(sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }')"
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q '^Status: active' && [[ -n "$ssh_port" ]]; then
ufw allow in on "$WG_INTERFACE" to any port "$ssh_port" proto tcp comment 'Server Monitor Mesh SSH' >/dev/null
fi
if ping -c 1 -W 4 "$HUB_ADDRESS" >/dev/null 2>&1; then
log "Узел $name подключён к Hub; адрес $address"
else
warn "Интерфейс поднят, но Hub пока не отвечает. Проверьте UDP-порт и команду sudo wg show."
fi
}
install_node() {
install_monitoring
install_node_mesh
log "Вторичный сервер установлен"
}
show_status() {
local ssh_port="unknown"
if command -v sshd >/dev/null 2>&1; then
ssh_port="$(sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }')"
fi
printf 'Пользователь: %s\n' "$MONITOR_USER"
printf 'Пользователь мониторинга: %s\n' "$MONITOR_USER"
printf 'SSH-порт: %s\n' "${ssh_port:-unknown}"
printf 'Команда: %s\n' "$MONITOR_COMMAND"
if [[ -r "$HUB_CONFIG" && -x "$HUB_HELPER" ]]; then
"$HUB_HELPER" status
elif [[ -r "$MESH_DIR/node.conf" ]]; then
cat "$MESH_DIR/node.conf"
wg show "$WG_INTERFACE" 2>/dev/null || true
else
printf 'ROLE=monitor-only\n'
fi
if [[ -x "$MONITOR_COMMAND" ]]; then
runuser -u "$MONITOR_USER" -- "$MONITOR_COMMAND"
else
@ -133,14 +628,8 @@ show_status() {
}
install_server_part() {
read_public_key
install_dependencies
verify_public_key
create_monitor_command
create_monitor_user
verify_sshd
systemctl reload ssh
log "Серверная часть установлена"
install_monitoring
log "Серверная часть мониторинга установлена"
log "Входящий порт не изменялся, новые правила UFW не создавались"
show_status
}
@ -157,7 +646,7 @@ uninstall_server_part() {
if getent passwd "$MONITOR_USER" >/dev/null; then
userdel --remove "$MONITOR_USER" 2>/dev/null || userdel "$MONITOR_USER"
fi
log "Серверная часть удалена"
log "Серверная часть мониторинга удалена"
}
main() {
@ -166,9 +655,12 @@ main() {
detect_system
case "$action" in
install) install_server_part ;;
hub) install_hub ;;
node) install_node ;;
hub-code) [[ $# -eq 2 ]] || die "Использование: $0 hub-code NAME"; exec "$HUB_HELPER" node-code "$2" ;;
status) show_status ;;
uninstall) uninstall_server_part ;;
*) die "Использование: $0 [install|status|uninstall]" ;;
*) die "Использование: $0 {install|hub|node|hub-code NAME|status|uninstall}" ;;
esac
}