diff --git a/README.md b/README.md index 3bf9567..3d5fb77 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/ochenstarik-server-monitor-manager.sh b/ochenstarik-server-monitor-manager.sh index eea5eac..59e531d 100644 --- a/ochenstarik-server-monitor-manager.sh +++ b/ochenstarik-server-monitor-manager.sh @@ -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 < /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 }