1380 lines
56 KiB
Bash
1380 lines
56 KiB
Bash
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
readonly APP_NAME="ochenstarik-server-monitor-manager"
|
||
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 MONITOR_SSH_CONFIG="/etc/ssh/sshd_config.d/90-ochenstarik-server-monitor.conf"
|
||
readonly MESH_DIR="/etc/${APP_NAME}"
|
||
readonly NODES_DIR="${MESH_DIR}/nodes"
|
||
readonly TOKENS_DIR="${MESH_DIR}/tokens"
|
||
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 CONTROL_POLICY_HELPER="/usr/local/libexec/ochenstarik-smm-policy-apply"
|
||
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"
|
||
readonly CONTROL_PORT="7443"
|
||
readonly CONTROL_USER="ochenstarik-smm-control"
|
||
readonly AGENT_USER="ochenstarik-smm-agent"
|
||
readonly CONTROL_STATE="${MONITOR_HOME}/control"
|
||
readonly AGENT_STATE="${MONITOR_HOME}/agent"
|
||
readonly CONTROL_BINARY="/usr/local/lib/${APP_NAME}/control/ochenstarik-smm-control"
|
||
readonly AGENT_BINARY="/usr/local/lib/${APP_NAME}/agent/ochenstarik-smm-agent"
|
||
readonly CONTROL_ENV="${MESH_DIR}/control.env"
|
||
readonly AGENT_ENV="${MESH_DIR}/agent.env"
|
||
readonly CONTROL_CA_CERT="${MESH_DIR}/control-ca.crt"
|
||
readonly CONTROL_SERVICE="/etc/systemd/system/ochenstarik-smm-control.service"
|
||
readonly AGENT_SERVICE="/etc/systemd/system/ochenstarik-smm-agent.service"
|
||
readonly SMM_RELEASE_VERSION="${SMM_RELEASE_VERSION:-v0.1.0-alpha.5}"
|
||
readonly SMM_RELEASE_BASE_URL="${SMM_RELEASE_BASE_URL:-https://github.com/ochenstarik-ui/server-monitor-manager/releases/download/${SMM_RELEASE_VERSION}}"
|
||
|
||
log() { printf '[+] %s\n' "$*"; }
|
||
warn() { printf '[!] %s\n' "$*" >&2; }
|
||
die() { printf '[x] %s\n' "$*" >&2; exit 1; }
|
||
|
||
require_root() {
|
||
[[ "$EUID" -eq 0 ]] || die "Запустите скрипт через sudo"
|
||
}
|
||
|
||
detect_system() {
|
||
[[ -r /etc/os-release ]] || die "Не найден /etc/os-release"
|
||
# shellcheck disable=SC1091
|
||
. /etc/os-release
|
||
case "${ID:-}" in
|
||
ubuntu|debian) ;;
|
||
*) die "Поддерживаются Ubuntu и Debian; обнаружено: ${ID:-unknown}" ;;
|
||
esac
|
||
command -v systemctl >/dev/null 2>&1 || die "Требуется systemd"
|
||
}
|
||
|
||
read_public_key() {
|
||
if [[ -n "${SERVER_MONITOR_PUBLIC_KEY:-}" ]]; then
|
||
PUBLIC_KEY="$SERVER_MONITOR_PUBLIC_KEY"
|
||
log "Публичный ключ получен из SERVER_MONITOR_PUBLIC_KEY"
|
||
else
|
||
printf '\nВ Windows-приложении нажмите «SSH-ключ» → «Копировать».\n'
|
||
if [[ -r /dev/tty ]]; then
|
||
IFS= read -r -p 'Вставьте публичный ключ: ' PUBLIC_KEY < /dev/tty
|
||
else
|
||
die "Нет интерактивного терминала. Передайте ключ через SERVER_MONITOR_PUBLIC_KEY"
|
||
fi
|
||
fi
|
||
PUBLIC_KEY="${PUBLIC_KEY//$'\r'/}"
|
||
[[ "$PUBLIC_KEY" =~ ^ssh-ed25519[[:space:]]+[A-Za-z0-9+/]+={0,3}([[:space:]].*)?$ ]] \
|
||
|| die "Ожидается публичный ключ формата ssh-ed25519 AAAA..."
|
||
}
|
||
|
||
install_monitor_dependencies() {
|
||
apt-get update
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||
ca-certificates openssh-server openssh-client coreutils gawk sudo
|
||
systemctl enable --now ssh
|
||
}
|
||
|
||
install_mesh_dependencies() {
|
||
apt-get update
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||
wireguard-tools nftables iproute2 iputils-ping jq openssl
|
||
}
|
||
|
||
verify_public_key() {
|
||
local key_file
|
||
key_file="$(mktemp)"
|
||
trap 'rm -f -- "${key_file:-}"' RETURN
|
||
printf '%s\n' "$PUBLIC_KEY" > "$key_file"
|
||
ssh-keygen -l -f "$key_file" >/dev/null \
|
||
|| die "ssh-keygen отклонил публичный ключ"
|
||
trap - RETURN
|
||
rm -f -- "$key_file"
|
||
}
|
||
|
||
create_monitor_command() {
|
||
install -d -m 0755 -o root -g root "$(dirname "$MONITOR_COMMAND")"
|
||
[[ ! -L "$MONITOR_COMMAND" ]] \
|
||
|| die "Отказ от записи через символическую ссылку: $MONITOR_COMMAND"
|
||
cat > "$MONITOR_COMMAND" <<'EOF'
|
||
#!/usr/bin/env bash
|
||
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 protocol port ttl extra <<< "$original_command"
|
||
[[ "$prefix" == mesh && "$action" == connect && -n "$ttl" && -z "${extra:-}" ]] \
|
||
|| { echo "Некорректная команда" >&2; exit 2; }
|
||
exec sudo -n "$HUB_HELPER" link-connect "$source" "$target" "$protocol" "$port" "$ttl"
|
||
;;
|
||
"mesh disconnect "*)
|
||
[[ -x "$HUB_HELPER" ]] || { echo "Mesh Hub не установлен" >&2; exit 1; }
|
||
read -r prefix action source target protocol port extra <<< "$original_command"
|
||
[[ "$prefix" == mesh && "$action" == disconnect && -n "$port" && -z "${extra:-}" ]] \
|
||
|| { echo "Некорректная команда" >&2; exit 2; }
|
||
exec sudo -n "$HUB_HELPER" link-disconnect "$source" "$target" "$protocol" "$port"
|
||
;;
|
||
*)
|
||
echo "Разрешены только metrics и команды mesh" >&2
|
||
exit 2
|
||
;;
|
||
esac
|
||
|
||
read_mem_value() {
|
||
awk -v key="$1" '$1 == key ":" { print $2; exit }' /proc/meminfo
|
||
}
|
||
|
||
printf 'PROTOCOL=1\n'
|
||
printf 'HOSTNAME=%s\n' "$(hostname)"
|
||
printf 'UPTIME_SECONDS=%s\n' "$(cut -d. -f1 /proc/uptime)"
|
||
printf 'LOAD1=%s\n' "$(cut -d' ' -f1 /proc/loadavg)"
|
||
printf 'CPU_COUNT=%s\n' "$(getconf _NPROCESSORS_ONLN)"
|
||
printf 'MEM_TOTAL_KB=%s\n' "$(read_mem_value MemTotal)"
|
||
printf 'MEM_AVAILABLE_KB=%s\n' "$(read_mem_value MemAvailable)"
|
||
printf 'SWAP_TOTAL_KB=%s\n' "$(read_mem_value SwapTotal)"
|
||
printf 'SWAP_FREE_KB=%s\n' "$(read_mem_value SwapFree)"
|
||
df -Pk / | awk 'NR == 2 {
|
||
printf "DISK_TOTAL_KB=%s\nDISK_AVAILABLE_KB=%s\n", $2, $4
|
||
}'
|
||
df -Pi / | awk 'NR == 2 {
|
||
printf "DISK_INODES_TOTAL=%s\nDISK_INODES_FREE=%s\n", $2, $4
|
||
}'
|
||
awk 'BEGIN { rx=0; tx=0 }
|
||
FNR == 1 && FILENAME !~ "/lo/" { rx += $1 }
|
||
FNR == 1 && FILENAME !~ "/lo/" { getline value < (FILENAME ~ /rx_bytes/ ? gensub(/rx_bytes$/, "tx_bytes", 1, FILENAME) : FILENAME); tx += value }
|
||
END { printf "NETWORK_RX_BYTES=%.0f\nNETWORK_TX_BYTES=%.0f\n", rx, tx }
|
||
' /sys/class/net/*/statistics/rx_bytes 2>/dev/null || printf 'NETWORK_RX_BYTES=0\nNETWORK_TX_BYTES=0\n'
|
||
printf 'SYSTEMD_SSH=%s\n' "$(systemctl is-active ssh 2>/dev/null || true)"
|
||
printf 'SYSTEMD_WIREGUARD=%s\n' "$(systemctl is-active wg-quick@smm0.service 2>/dev/null || true)"
|
||
printf 'KERNEL=%s\n' "$(uname -r)"
|
||
EOF
|
||
chown root:root "$MONITOR_COMMAND"
|
||
chmod 0755 "$MONITOR_COMMAND"
|
||
}
|
||
|
||
create_monitor_user() {
|
||
if ! getent passwd "$MONITOR_USER" >/dev/null; then
|
||
useradd \
|
||
--system \
|
||
--create-home \
|
||
--home-dir "$MONITOR_HOME" \
|
||
--shell /bin/bash \
|
||
"$MONITOR_USER"
|
||
fi
|
||
usermod --home "$MONITOR_HOME" --shell /bin/bash "$MONITOR_USER"
|
||
passwd -l "$MONITOR_USER" >/dev/null 2>&1 || true
|
||
|
||
install -d -m 0750 -o "$MONITOR_USER" -g "$MONITOR_USER" "$MONITOR_HOME"
|
||
install -d -m 0700 -o "$MONITOR_USER" -g "$MONITOR_USER" "${MONITOR_HOME}/.ssh"
|
||
[[ ! -L "$AUTHORIZED_KEYS" ]] \
|
||
|| die "Отказ от записи через символическую ссылку: $AUTHORIZED_KEYS"
|
||
printf 'restrict,command="%s" %s\n' "$MONITOR_COMMAND" "$PUBLIC_KEY" > "$AUTHORIZED_KEYS"
|
||
chown "$MONITOR_USER:$MONITOR_USER" "$AUTHORIZED_KEYS"
|
||
chmod 0600 "$AUTHORIZED_KEYS"
|
||
|
||
install -d -m 0755 -o root -g root /etc/ssh/sshd_config.d
|
||
cat > "$MONITOR_SSH_CONFIG" <<'EOF'
|
||
# Required by the restricted Server Monitor Manager identity only.
|
||
Match User ochenstarik-monitor
|
||
PubkeyAuthentication yes
|
||
Match all
|
||
EOF
|
||
chown root:root "$MONITOR_SSH_CONFIG"
|
||
chmod 0644 "$MONITOR_SSH_CONFIG"
|
||
}
|
||
|
||
verify_sshd() {
|
||
sshd -t
|
||
sshd -T -C "user=${MONITOR_USER},host=localhost,addr=127.0.0.1" \
|
||
| awk '$1 == "pubkeyauthentication" && $2 == "yes" { enabled=1 } END { exit !enabled }' \
|
||
|| 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 TOKENS_DIR="${STATE_DIR}/tokens"
|
||
readonly HUB_CONFIG="${STATE_DIR}/hub.conf"
|
||
readonly HUB_PRIVATE_KEY="${STATE_DIR}/hub.key"
|
||
readonly LINKS_FILE="${STATE_DIR}/links"
|
||
readonly AUDIT_FILE="${STATE_DIR}/audit.jsonl"
|
||
readonly VERSION_FILE="${STATE_DIR}/policy.version"
|
||
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")" ]]; }
|
||
|
||
next_policy_version() {
|
||
local version=0 tmp
|
||
[[ ! -r "$VERSION_FILE" ]] || read -r version < "$VERSION_FILE"
|
||
[[ "$version" =~ ^[0-9]+$ ]] || version=0
|
||
version=$((version + 1))
|
||
tmp="$(mktemp)"
|
||
printf '%s\n' "$version" > "$tmp"
|
||
install -m 0600 -o root -g root "$tmp" "$VERSION_FILE"
|
||
rm -f -- "$tmp"
|
||
printf '%s' "$version"
|
||
}
|
||
|
||
audit_link() {
|
||
local action="$1" state="$2" source="$3" target="$4" protocol="$5" port="$6" version="$7"
|
||
printf '{"time":"%s","action":"%s","state":"%s","source":"%s","target":"%s","protocol":"%s","port":%s,"version":%s}\n' \
|
||
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$action" "$state" "$source" "$target" "$protocol" "$port" "$version" \
|
||
>> "$AUDIT_FILE"
|
||
chmod 0600 "$AUDIT_FILE"
|
||
}
|
||
|
||
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 "$TOKENS_DIR"/*.token*; 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 '='
|
||
}
|
||
|
||
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 }'
|
||
}
|
||
|
||
token_cleanup() {
|
||
local file expires now
|
||
now="$(date +%s)"
|
||
for file in "$TOKENS_DIR"/*.token; do
|
||
[[ -e "$file" ]] || continue
|
||
expires="$(config_value EXPIRES "$file")"
|
||
[[ "$expires" =~ ^[0-9]+$ ]] || { rm -f -- "$file"; continue; }
|
||
(( expires > now )) || rm -f -- "$file"
|
||
done
|
||
}
|
||
|
||
node_code() {
|
||
local name="$1" address endpoint port payload code token token_hash expires file
|
||
valid_name "$name" || die "Имя: строчные латинские буквы, цифры и дефис, максимум 32 символа"
|
||
node_exists "$name" && die "Узел $name уже существует"
|
||
token_cleanup
|
||
for file in "$TOKENS_DIR"/*.token; do
|
||
[[ -e "$file" ]] || continue
|
||
[[ "$(config_value NAME "$file")" != "$name" ]] \
|
||
|| die "Для узла $name уже существует действующий enrollment-код"
|
||
done
|
||
address="$(next_address)"
|
||
endpoint="$(config_value HUB_ENDPOINT "$HUB_CONFIG")"
|
||
port="$(config_value WG_PORT "$HUB_CONFIG")"
|
||
token="$(openssl rand -hex 32)"
|
||
token_hash="$(printf '%s' "$token" | sha256sum | awk '{ print $1 }')"
|
||
expires=$(( $(date +%s) + 600 ))
|
||
file="${TOKENS_DIR}/${token_hash}.token"
|
||
umask 077
|
||
{
|
||
printf 'NAME=%s\n' "$name"
|
||
printf 'ADDRESS=%s\n' "$address"
|
||
printf 'EXPIRES=%s\n' "$expires"
|
||
} > "$file"
|
||
payload="$(printf 'VERSION=2\nNAME=%s\nADDRESS=%s/32\nTOKEN=%s\nEXPIRES=%s\nHUB_PUBLIC_KEY=%s\nENDPOINT=%s:%s\nALLOWED_IPS=10.77.0.0/24\n' \
|
||
"$name" "$address" "$token" "$expires" "$(wg pubkey < "$HUB_PRIVATE_KEY")" "$endpoint" "$port")"
|
||
code="SMM2-$(printf '%s' "$payload" | base64url_encode)"
|
||
unset token payload
|
||
printf '\nОдноразовый enrollment-код для узла %s (действует 10 минут):\n%s\n\n' "$name" "$code"
|
||
printf 'Приватный WireGuard-ключ будет создан только на Node. Не сохраняйте код в чатах или логах.\n'
|
||
}
|
||
|
||
node_enroll() {
|
||
local request="${SMM_ENROLL_REQUEST:-}" payload version token token_hash token_file consuming_file
|
||
local name address public_key stored_name stored_address expires now node_path ack
|
||
local hub_public_key endpoint allowed_ips
|
||
if [[ -z "$request" && -r /dev/tty ]]; then
|
||
IFS= read -r -s -p 'Вставьте request-код SMMREQ1: ' request < /dev/tty
|
||
printf '\n'
|
||
fi
|
||
[[ "$request" == SMMREQ1-* ]] || die "Ожидается request-код SMMREQ1-..."
|
||
payload="$(base64url_decode "${request#SMMREQ1-}")" || die "Не удалось декодировать request-код"
|
||
version="$(payload_value "$payload" VERSION)"
|
||
token="$(payload_value "$payload" TOKEN)"
|
||
name="$(payload_value "$payload" NAME)"
|
||
address="$(payload_value "$payload" ADDRESS)"
|
||
public_key="$(payload_value "$payload" PUBLIC_KEY)"
|
||
[[ "$version" == 1 ]] || die "Неподдерживаемая версия request-кода"
|
||
[[ "$token" =~ ^[a-f0-9]{64}$ ]] || die "Некорректный enrollment token"
|
||
valid_name "$name" || die "Некорректное имя Node"
|
||
[[ "$address" =~ ^10\.77\.0\.([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])/32$ ]] \
|
||
|| die "Некорректный адрес Node"
|
||
[[ "$(printf '%s' "$public_key" | base64 -d 2>/dev/null | wc -c)" -eq 32 ]] \
|
||
|| die "Некорректный публичный WireGuard-ключ Node"
|
||
token_hash="$(printf '%s' "$token" | sha256sum | awk '{ print $1 }')"
|
||
token_file="${TOKENS_DIR}/${token_hash}.token"
|
||
[[ -f "$token_file" ]] || die "Enrollment token не найден, уже использован или истёк"
|
||
stored_name="$(config_value NAME "$token_file")"
|
||
stored_address="$(config_value ADDRESS "$token_file")"
|
||
expires="$(config_value EXPIRES "$token_file")"
|
||
now="$(date +%s)"
|
||
[[ "$stored_name" == "$name" && "${address%/32}" == "$stored_address" ]] \
|
||
|| die "Параметры Node не совпадают с enrollment token"
|
||
[[ "$expires" =~ ^[0-9]+$ ]] && (( expires > now )) \
|
||
|| { rm -f -- "$token_file"; die "Enrollment token истёк"; }
|
||
node_exists "$name" && die "Узел $name уже зарегистрирован"
|
||
consuming_file="${token_file}.consuming"
|
||
mv -- "$token_file" "$consuming_file" \
|
||
|| die "Enrollment token уже обрабатывается"
|
||
node_path="$(node_file "$name")"
|
||
umask 077
|
||
{
|
||
printf 'NAME=%s\n' "$name"
|
||
printf 'ADDRESS=%s\n' "${address%/32}"
|
||
printf 'PUBLIC_KEY=%s\n' "$public_key"
|
||
} > "$node_path"
|
||
render_wireguard_config
|
||
rm -f -- "$consuming_file"
|
||
hub_public_key="$(wg pubkey < "$HUB_PRIVATE_KEY")"
|
||
endpoint="$(config_value HUB_ENDPOINT "$HUB_CONFIG"):$(config_value WG_PORT "$HUB_CONFIG")"
|
||
allowed_ips="10.77.0.0/24"
|
||
ack="$(printf '%s|%s|%s|%s|%s|%s|%s' \
|
||
"$token" "$public_key" "$name" "$address" "$hub_public_key" "$endpoint" "$allowed_ips" \
|
||
| sha256sum | awk '{ print $1 }')"
|
||
unset token payload
|
||
printf '\nNode зарегистрирован. Верните этот код в установщик Node:\nSMMACK1-%s\n\n' "$ack"
|
||
}
|
||
|
||
prune_links() {
|
||
local tmp source target cidr protocol port expires version now target_ip
|
||
tmp="$(mktemp)"
|
||
now="$(date +%s)"
|
||
if [[ -f "$LINKS_FILE" ]]; then
|
||
while read -r source target cidr protocol port expires version extra; do
|
||
[[ -z "${extra:-}" ]] || continue
|
||
valid_name "$source" && valid_name "$target" || continue
|
||
node_exists "$source" && node_exists "$target" || continue
|
||
target_ip="$(node_value "$target" ADDRESS)"
|
||
[[ "$cidr" == "${target_ip}/32" ]] || continue
|
||
[[ "$protocol" == tcp || "$protocol" == udp ]] || continue
|
||
[[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) || continue
|
||
[[ "$expires" =~ ^[0-9]+$ ]] || continue
|
||
[[ "${version:-}" =~ ^[0-9]+$ ]] || version=0
|
||
if (( expires != 0 && expires <= now )); then
|
||
audit_link expire Expired "$source" "$target" "$protocol" "$port" "$version"
|
||
continue
|
||
fi
|
||
printf '%s %s %s %s %s %s %s\n' "$source" "$target" "$cidr" "$protocol" "$port" "$expires" "$version" >> "$tmp"
|
||
done < "$LINKS_FILE"
|
||
fi
|
||
sort -u -o "$tmp" "$tmp"
|
||
install -m 0600 -o root -g root "$tmp" "$LINKS_FILE"
|
||
rm -f -- "$tmp"
|
||
}
|
||
|
||
restore_firewall() {
|
||
local tmp body source target cidr protocol port expires version source_ip
|
||
prune_links
|
||
tmp="$(mktemp)"
|
||
body="$(mktemp)"
|
||
{
|
||
printf 'table inet %s {\n' "$NFT_TABLE"
|
||
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"
|
||
while read -r source target cidr protocol port expires version; do
|
||
[[ -n "$source" ]] || continue
|
||
source_ip="$(node_value "$source" ADDRESS)"
|
||
printf ' iifname "%s" oifname "%s" ip saddr %s ip daddr %s %s dport %s accept\n' \
|
||
"$WG_INTERFACE" "$WG_INTERFACE" "$source_ip" "$cidr" "$protocol" "$port"
|
||
done < "$LINKS_FILE"
|
||
printf ' iifname "%s" oifname "%s" drop\n' "$WG_INTERFACE" "$WG_INTERFACE"
|
||
printf ' }\n}\n'
|
||
} > "$body"
|
||
if nft list table inet "$NFT_TABLE" >/dev/null 2>&1; then
|
||
printf 'delete table inet %s\n' "$NFT_TABLE" > "$tmp"
|
||
fi
|
||
cat "$body" >> "$tmp"
|
||
rm -f -- "$body"
|
||
if ! nft --check -f "$tmp" || ! nft -f "$tmp"; then
|
||
rm -f -- "$tmp"
|
||
return 1
|
||
fi
|
||
rm -f -- "$tmp"
|
||
}
|
||
|
||
link_connect() {
|
||
local source="$1" target="$2" protocol="$3" port="$4" ttl_minutes="$5"
|
||
local target_ip cidr expires version tmp
|
||
valid_name "$source" && valid_name "$target" || die "Некорректное имя узла"
|
||
[[ "$source" != "$target" ]] || die "Источник и назначение совпадают"
|
||
node_exists "$source" || die "Узел $source не найден"
|
||
node_exists "$target" || die "Узел $target не найден"
|
||
[[ "$protocol" == tcp || "$protocol" == udp ]] || die "Протокол должен быть tcp или udp"
|
||
[[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) || die "Порт должен быть от 1 до 65535"
|
||
[[ "$ttl_minutes" =~ ^[0-9]+$ ]] && (( ttl_minutes <= 525600 )) \
|
||
|| die "TTL должен быть от 0 до 525600 минут"
|
||
target_ip="$(node_value "$target" ADDRESS)"
|
||
cidr="${target_ip}/32"
|
||
expires=0
|
||
(( ttl_minutes == 0 )) || expires=$(( $(date +%s) + ttl_minutes * 60 ))
|
||
version="$(next_policy_version)"
|
||
audit_link connect Connecting "$source" "$target" "$protocol" "$port" "$version"
|
||
tmp="$(mktemp)"
|
||
if [[ -f "$LINKS_FILE" ]]; then
|
||
awk -v source="$source" -v target="$target" -v protocol="$protocol" -v port="$port" \
|
||
'!($1 == source && $2 == target && $4 == protocol && $5 == port)' "$LINKS_FILE" > "$tmp"
|
||
fi
|
||
printf '%s %s %s %s %s %s %s\n' "$source" "$target" "$cidr" "$protocol" "$port" "$expires" "$version" >> "$tmp"
|
||
sort -u -o "$tmp" "$tmp"
|
||
install -m 0600 -o root -g root "$tmp" "$LINKS_FILE"
|
||
rm -f -- "$tmp"
|
||
if ! restore_firewall; then
|
||
audit_link connect Failed "$source" "$target" "$protocol" "$port" "$version"
|
||
die "Не удалось применить nftables policy"
|
||
fi
|
||
audit_link connect Active "$source" "$target" "$protocol" "$port" "$version"
|
||
log "Связь $source → $target: $protocol/$port включена"
|
||
printf 'LINK_STATE=Active|VERSION=%s\n' "$version"
|
||
}
|
||
|
||
link_disconnect() {
|
||
local source="$1" target="$2" protocol="$3" port="$4" version tmp
|
||
valid_name "$source" && valid_name "$target" || die "Некорректное имя узла"
|
||
[[ "$protocol" == tcp || "$protocol" == udp ]] || die "Протокол должен быть tcp или udp"
|
||
[[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) || die "Некорректный порт"
|
||
version="$(next_policy_version)"
|
||
audit_link disconnect Disconnecting "$source" "$target" "$protocol" "$port" "$version"
|
||
tmp="$(mktemp)"
|
||
if [[ -f "$LINKS_FILE" ]]; then
|
||
awk -v source="$source" -v target="$target" -v protocol="$protocol" -v port="$port" \
|
||
'!($1 == source && $2 == target && $4 == protocol && $5 == port)' "$LINKS_FILE" > "$tmp"
|
||
fi
|
||
install -m 0600 -o root -g root "$tmp" "$LINKS_FILE"
|
||
rm -f -- "$tmp"
|
||
if ! restore_firewall; then
|
||
audit_link disconnect Failed "$source" "$target" "$protocol" "$port" "$version"
|
||
die "Не удалось применить отключение в nftables"
|
||
fi
|
||
audit_link disconnect Disabled "$source" "$target" "$protocol" "$port" "$version"
|
||
log "Связь $source → $target: $protocol/$port отключена"
|
||
printf 'LINK_STATE=Disabled|VERSION=%s\n' "$version"
|
||
}
|
||
|
||
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 cidr protocol port expires version
|
||
prune_links
|
||
[[ -f "$LINKS_FILE" ]] || return 0
|
||
while read -r source target cidr protocol port expires version; do
|
||
[[ -n "$source" && -n "$target" ]] || continue
|
||
printf 'LINK=%s|%s|%s|%s|%s|%s|Active|%s\n' \
|
||
"$source" "$target" "$cidr" "$protocol" "$port" "$expires" "$version"
|
||
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-enroll) [[ $# -eq 1 ]] || die "Использование: $0 node-enroll"; node_enroll ;;
|
||
node-remove) [[ $# -eq 2 ]] || die "Использование: $0 node-remove NAME"; remove_node "$2" ;;
|
||
nodes) list_nodes ;;
|
||
links) list_links ;;
|
||
link-connect) [[ $# -eq 6 ]] || die "Использование: $0 link-connect SOURCE TARGET tcp|udp PORT TTL_MINUTES"; link_connect "$2" "$3" "$4" "$5" "$6" ;;
|
||
link-disconnect) [[ $# -eq 5 ]] || die "Использование: $0 link-disconnect SOURCE TARGET tcp|udp PORT"; link_disconnect "$2" "$3" "$4" "$5" ;;
|
||
render) render_wireguard_config ;;
|
||
firewall-restore) restore_firewall ;;
|
||
status) status ;;
|
||
*) die "Команды: node-code, node-enroll, 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
|
||
ExecStart=${HUB_HELPER} firewall-restore
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
chmod 0644 /etc/systemd/system/ochenstarik-smm-firewall.service
|
||
cat > /etc/systemd/system/ochenstarik-smm-firewall.timer <<'EOF'
|
||
[Unit]
|
||
Description=Expire Server Monitor mesh policies
|
||
|
||
[Timer]
|
||
OnBootSec=1min
|
||
OnUnitActiveSec=1min
|
||
AccuracySec=10s
|
||
Persistent=true
|
||
Unit=ochenstarik-smm-firewall.service
|
||
|
||
[Install]
|
||
WantedBy=timers.target
|
||
EOF
|
||
chmod 0644 /etc/systemd/system/ochenstarik-smm-firewall.timer
|
||
systemctl daemon-reload
|
||
systemctl enable --now ochenstarik-smm-firewall.service
|
||
systemctl enable --now ochenstarik-smm-firewall.timer
|
||
}
|
||
|
||
configure_hub_sudo() {
|
||
{
|
||
printf '%s ALL=(root) NOPASSWD: %s *\n' "$MONITOR_USER" "$HUB_HELPER"
|
||
if getent passwd "$CONTROL_USER" >/dev/null; then
|
||
printf '%s ALL=(root) NOPASSWD: %s *\n' "$CONTROL_USER" "$CONTROL_POLICY_HELPER"
|
||
fi
|
||
} > /etc/sudoers.d/ochenstarik-smm-hub
|
||
chmod 0440 /etc/sudoers.d/ochenstarik-smm-hub
|
||
visudo -cf /etc/sudoers.d/ochenstarik-smm-hub >/dev/null \
|
||
|| die "Некорректный sudoers-файл Mesh Hub"
|
||
}
|
||
|
||
create_control_policy_helper() {
|
||
cat > "$CONTROL_POLICY_HELPER" <<'EOF'
|
||
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
readonly HUB_HELPER="/usr/local/libexec/ochenstarik-smm-hub"
|
||
action="${1:-}"
|
||
|
||
valid_name() { [[ "$1" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]]; }
|
||
valid_protocol() { [[ "$1" == tcp || "$1" == udp ]]; }
|
||
valid_port() { [[ "$1" =~ ^[0-9]+$ ]] && (( $1 >= 1 && $1 <= 65535 )); }
|
||
|
||
case "$action" in
|
||
link-connect)
|
||
[[ $# -eq 6 ]] || exit 2
|
||
valid_name "$2" && valid_name "$3" && valid_protocol "$4" && valid_port "$5" \
|
||
&& [[ "$6" =~ ^[0-9]+$ ]] && (( $6 <= 525600 )) || exit 2
|
||
;;
|
||
link-disconnect)
|
||
[[ $# -eq 5 ]] || exit 2
|
||
valid_name "$2" && valid_name "$3" && valid_protocol "$4" && valid_port "$5" || exit 2
|
||
;;
|
||
*) exit 2 ;;
|
||
esac
|
||
|
||
exec "$HUB_HELPER" "$@"
|
||
EOF
|
||
chown root:root "$CONTROL_POLICY_HELPER"
|
||
chmod 0750 "$CONTROL_POLICY_HELPER"
|
||
}
|
||
|
||
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"
|
||
}
|
||
|
||
runtime_id() {
|
||
case "$(uname -m)" in
|
||
x86_64|amd64) printf 'linux-x64\n' ;;
|
||
aarch64|arm64) printf 'linux-arm64\n' ;;
|
||
*) die "Control layer поддерживает только amd64 и arm64" ;;
|
||
esac
|
||
}
|
||
|
||
download_control_layer() {
|
||
local runtime archive archive_name checksum temporary
|
||
runtime="$(runtime_id)"
|
||
archive_name="server-monitor-manager-${runtime}.tar.gz"
|
||
temporary="$(mktemp -d)"
|
||
archive="${temporary}/${archive_name}"
|
||
trap 'rm -rf -- "${temporary:-}"' RETURN
|
||
apt-get update
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl openssl tar
|
||
curl -4 -fL --retry 5 --retry-delay 5 --connect-timeout 30 \
|
||
"${SMM_RELEASE_BASE_URL}/${archive_name}" -o "$archive"
|
||
curl -4 -fL --retry 5 --retry-delay 5 --connect-timeout 30 \
|
||
"${SMM_RELEASE_BASE_URL}/${archive_name}.sha256" -o "${archive}.sha256"
|
||
checksum="$(awk 'NR == 1 { print $1 }' "${archive}.sha256")"
|
||
[[ "$checksum" =~ ^[a-fA-F0-9]{64}$ ]] || die "Некорректный release checksum"
|
||
[[ "$(sha256sum "$archive" | awk '{ print $1 }')" == "$checksum" ]] \
|
||
|| die "Checksum control layer не совпал"
|
||
tar -xzf "$archive" -C "$temporary"
|
||
[[ -x "${temporary}/agent/ochenstarik-smm-agent" ]] || die "В release нет Agent"
|
||
[[ -x "${temporary}/control/ochenstarik-smm-control" ]] || die "В release нет Control"
|
||
install -d -m 0755 -o root -g root "$(dirname "$AGENT_BINARY")" "$(dirname "$CONTROL_BINARY")"
|
||
install -m 0755 -o root -g root "${temporary}/agent/ochenstarik-smm-agent" "$AGENT_BINARY"
|
||
install -m 0755 -o root -g root "${temporary}/control/ochenstarik-smm-control" "$CONTROL_BINARY"
|
||
trap - RETURN
|
||
rm -rf -- "$temporary"
|
||
}
|
||
|
||
ensure_system_user() {
|
||
local user="$1" home="$2"
|
||
if ! getent passwd "$user" >/dev/null; then
|
||
useradd --system --home-dir "$home" --create-home --shell /usr/sbin/nologin "$user"
|
||
fi
|
||
install -d -m 0700 -o "$user" -g "$user" "$home"
|
||
}
|
||
|
||
create_control_certificates() {
|
||
local endpoint="$1" san temporary
|
||
[[ -f "${CONTROL_STATE}/control-ca.pfx" && -f "${CONTROL_STATE}/server.pfx" && -f "$CONTROL_CA_CERT" ]] \
|
||
&& return 0
|
||
temporary="$(mktemp -d)"
|
||
trap 'rm -rf -- "${temporary:-}"' RETURN
|
||
if [[ "$endpoint" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||
san="IP:${endpoint}"
|
||
else
|
||
san="DNS:${endpoint}"
|
||
fi
|
||
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -sha256 -nodes \
|
||
-days 3650 -subj '/CN=Ochenstarik SMM Control CA' \
|
||
-keyout "${temporary}/ca.key" -out "${temporary}/ca.crt" >/dev/null 2>&1
|
||
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -sha256 -nodes \
|
||
-subj "/CN=${endpoint}" -addext "subjectAltName=${san}" \
|
||
-keyout "${temporary}/server.key" -out "${temporary}/server.csr" >/dev/null 2>&1
|
||
openssl x509 -req -sha256 -days 825 -in "${temporary}/server.csr" \
|
||
-CA "${temporary}/ca.crt" -CAkey "${temporary}/ca.key" -CAcreateserial \
|
||
-copy_extensions copy -out "${temporary}/server.crt" >/dev/null 2>&1
|
||
openssl pkcs12 -export -passout pass: -name smm-control-ca \
|
||
-inkey "${temporary}/ca.key" -in "${temporary}/ca.crt" \
|
||
-out "${temporary}/control-ca.pfx"
|
||
openssl pkcs12 -export -passout pass: -name smm-control \
|
||
-inkey "${temporary}/server.key" -in "${temporary}/server.crt" \
|
||
-certfile "${temporary}/ca.crt" -out "${temporary}/server.pfx"
|
||
install -m 0600 -o "$CONTROL_USER" -g "$CONTROL_USER" \
|
||
"${temporary}/control-ca.pfx" "${CONTROL_STATE}/control-ca.pfx"
|
||
install -m 0600 -o "$CONTROL_USER" -g "$CONTROL_USER" \
|
||
"${temporary}/server.pfx" "${CONTROL_STATE}/server.pfx"
|
||
install -m 0644 -o root -g root "${temporary}/ca.crt" "$CONTROL_CA_CERT"
|
||
trap - RETURN
|
||
rm -rf -- "$temporary"
|
||
}
|
||
|
||
configure_control_service() {
|
||
cat > "$CONTROL_ENV" <<EOF
|
||
ASPNETCORE_URLS=https://0.0.0.0:${CONTROL_PORT}
|
||
ASPNETCORE_Kestrel__Certificates__Default__Path=${CONTROL_STATE}/server.pfx
|
||
Control__DatabasePath=${CONTROL_STATE}/control.db
|
||
Control__CertificateAuthorityPath=${CONTROL_STATE}/control-ca.pfx
|
||
Control__HeartbeatSeconds=30
|
||
Control__HubHelperPath=${CONTROL_POLICY_HELPER}
|
||
EOF
|
||
chmod 0600 "$CONTROL_ENV"
|
||
cat > "$CONTROL_SERVICE" <<EOF
|
||
[Unit]
|
||
Description=Ochenstarik Server Monitor Manager Control Hub
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=${CONTROL_USER}
|
||
Group=${CONTROL_USER}
|
||
EnvironmentFile=${CONTROL_ENV}
|
||
ExecStart=${CONTROL_BINARY}
|
||
Restart=on-failure
|
||
RestartSec=10s
|
||
PrivateTmp=true
|
||
PrivateDevices=true
|
||
ProtectSystem=strict
|
||
ProtectHome=true
|
||
ProtectKernelTunables=true
|
||
ProtectKernelModules=true
|
||
ProtectControlGroups=true
|
||
LockPersonality=true
|
||
RestrictSUIDSGID=true
|
||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||
ReadWritePaths=${CONTROL_STATE}
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
chmod 0644 "$CONTROL_SERVICE"
|
||
systemctl daemon-reload
|
||
systemctl enable --now ochenstarik-smm-control.service
|
||
systemctl restart ochenstarik-smm-control.service
|
||
}
|
||
|
||
install_control_hub() {
|
||
local endpoint
|
||
[[ -r "$HUB_CONFIG" ]] || die "Сначала установите роль Hub"
|
||
endpoint="$(awk -F= '$1 == "HUB_ENDPOINT" { print $2; exit }' "$HUB_CONFIG")"
|
||
[[ -n "$endpoint" ]] || die "В hub.conf отсутствует HUB_ENDPOINT"
|
||
download_control_layer
|
||
ensure_system_user "$CONTROL_USER" "$CONTROL_STATE"
|
||
install -d -m 0700 -o root -g root "$MESH_DIR"
|
||
create_control_policy_helper
|
||
configure_hub_sudo
|
||
create_control_certificates "$endpoint"
|
||
configure_control_service
|
||
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q '^Status: active'; then
|
||
ufw allow "${CONTROL_PORT}/tcp" comment 'Server Monitor Control' >/dev/null
|
||
fi
|
||
log "Control Hub установлен: https://${endpoint}:${CONTROL_PORT}"
|
||
log "Создать код Agent: sudo $0 control-code ИМЯ"
|
||
}
|
||
|
||
install_hub() {
|
||
install_monitoring
|
||
install_mesh_dependencies
|
||
read_hub_endpoint
|
||
install -d -m 0700 -o root -g root "$MESH_DIR" "$NODES_DIR" "$TOKENS_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"
|
||
touch "${MESH_DIR}/audit.jsonl"
|
||
chmod 0600 "${MESH_DIR}/audit.jsonl"
|
||
[[ -f "${MESH_DIR}/policy.version" ]] || printf '0\n' > "${MESH_DIR}/policy.version"
|
||
chmod 0600 "${MESH_DIR}/policy.version"
|
||
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 ai-agent"
|
||
log "Включить SSH на 2 часа: sudo ochenstarik-smm link-connect ai-agent home tcp 22 120"
|
||
log "Установить постоянный control layer: sudo $0 install-control-hub"
|
||
}
|
||
|
||
base64url_encode() {
|
||
base64 -w0 | tr '+/' '-_' | tr -d '='
|
||
}
|
||
|
||
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 }'
|
||
}
|
||
|
||
create_control_join_code() {
|
||
local name="$1" endpoint token ca payload
|
||
[[ -x "$CONTROL_BINARY" && -r "$CONTROL_ENV" && -r "$CONTROL_CA_CERT" ]] \
|
||
|| die "Control Hub не установлен"
|
||
[[ "$name" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]] || die "Некорректное имя Agent"
|
||
endpoint="$(awk -F= '$1 == "HUB_ENDPOINT" { print $2; exit }' "$HUB_CONFIG")"
|
||
token="$(
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
. "$CONTROL_ENV"
|
||
set +a
|
||
runuser -u "$CONTROL_USER" -m -- "$CONTROL_BINARY" token-create "$name"
|
||
)"
|
||
[[ "$token" =~ ^[A-Za-z0-9_-]{43}$ ]] || die "Control Hub не создал enrollment token"
|
||
ca="$(base64 -w0 "$CONTROL_CA_CERT")"
|
||
payload="$(printf 'VERSION=1\nNAME=%s\nURL=https://%s:%s\nTOKEN=%s\nCA=%s\n' \
|
||
"$name" "$endpoint" "$CONTROL_PORT" "$token" "$ca")"
|
||
printf 'SMMCTL1-%s\n' "$(printf '%s' "$payload" | base64url_encode)"
|
||
unset token payload ca
|
||
}
|
||
|
||
create_device_join_code() {
|
||
local device_id="$1" endpoint token ca payload
|
||
[[ -x "$CONTROL_BINARY" && -r "$CONTROL_ENV" && -r "$CONTROL_CA_CERT" ]] \
|
||
|| die "Control Hub не установлен"
|
||
[[ "$device_id" =~ ^[a-z0-9][a-z0-9-]{0,62}$ ]] || die "Некорректное имя устройства"
|
||
endpoint="$(awk -F= '$1 == "HUB_ENDPOINT" { print $2; exit }' "$HUB_CONFIG")"
|
||
token="$(
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
. "$CONTROL_ENV"
|
||
set +a
|
||
runuser -u "$CONTROL_USER" -m -- "$CONTROL_BINARY" device-token-create "$device_id"
|
||
)"
|
||
[[ "$token" =~ ^[A-Za-z0-9_-]{43}$ ]] || die "Control Hub не создал device token"
|
||
ca="$(base64 -w0 "$CONTROL_CA_CERT")"
|
||
payload="$(printf 'VERSION=1\nDEVICE=%s\nURL=https://%s:%s\nTOKEN=%s\nCA=%s\n' \
|
||
"$device_id" "$endpoint" "$CONTROL_PORT" "$token" "$ca")"
|
||
printf 'SMMDEV1-%s\n' "$(printf '%s' "$payload" | base64url_encode)"
|
||
unset token payload ca
|
||
}
|
||
|
||
read_control_join_code() {
|
||
if [[ -n "${SMM_CONTROL_CODE:-}" ]]; then
|
||
CONTROL_JOIN_CODE="$SMM_CONTROL_CODE"
|
||
elif [[ -r /dev/tty ]]; then
|
||
printf '\nНа Hub выполните: sudo %s control-code ИМЯ\n' "$0"
|
||
IFS= read -r -s -p 'Вставьте одноразовый код SMMCTL1: ' CONTROL_JOIN_CODE < /dev/tty
|
||
printf '\n'
|
||
else
|
||
die "Передайте код через SMM_CONTROL_CODE"
|
||
fi
|
||
CONTROL_JOIN_CODE="${CONTROL_JOIN_CODE//$'\r'/}"
|
||
[[ "$CONTROL_JOIN_CODE" == SMMCTL1-* ]] || die "Ожидается код формата SMMCTL1-..."
|
||
}
|
||
|
||
configure_agent_service() {
|
||
local node_id="$1" control_url="$2" ca_path="${AGENT_STATE}/control-ca.crt"
|
||
cat > "$AGENT_ENV" <<EOF
|
||
SMM_NodeId=${node_id}
|
||
SMM_ControlUrl=${control_url}
|
||
SMM_StateDirectory=${AGENT_STATE}
|
||
SMM_CertificateAuthorityPath=${ca_path}
|
||
SMM_HeartbeatSeconds=30
|
||
EOF
|
||
chmod 0600 "$AGENT_ENV"
|
||
cat > "$AGENT_SERVICE" <<EOF
|
||
[Unit]
|
||
Description=Ochenstarik Server Monitor Manager Agent
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=${AGENT_USER}
|
||
Group=${AGENT_USER}
|
||
EnvironmentFile=${AGENT_ENV}
|
||
ExecStart=${AGENT_BINARY}
|
||
Restart=on-failure
|
||
RestartSec=10s
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
PrivateDevices=true
|
||
ProtectSystem=strict
|
||
ProtectHome=true
|
||
ProtectKernelTunables=true
|
||
ProtectKernelModules=true
|
||
ProtectControlGroups=true
|
||
LockPersonality=true
|
||
RestrictSUIDSGID=true
|
||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||
ReadWritePaths=${AGENT_STATE}
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
chmod 0644 "$AGENT_SERVICE"
|
||
}
|
||
|
||
install_control_agent() {
|
||
local payload version name control_url token ca temporary ca_path
|
||
read_control_join_code
|
||
payload="$(base64url_decode "${CONTROL_JOIN_CODE#SMMCTL1-}")" \
|
||
|| die "Не удалось декодировать control code"
|
||
version="$(payload_value "$payload" VERSION)"
|
||
name="$(payload_value "$payload" NAME)"
|
||
control_url="$(payload_value "$payload" URL)"
|
||
token="$(payload_value "$payload" TOKEN)"
|
||
ca="$(payload_value "$payload" CA)"
|
||
[[ "$version" == 1 ]] || die "Неподдерживаемая версия control code"
|
||
[[ "$name" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]] || die "Некорректное имя Agent"
|
||
[[ "$control_url" =~ ^https://[A-Za-z0-9.-]+:[0-9]{1,5}$ ]] || die "Некорректный Control URL"
|
||
[[ "$token" =~ ^[A-Za-z0-9_-]{43}$ ]] || die "Некорректный control token"
|
||
temporary="$(mktemp)"
|
||
trap 'rm -f -- "${temporary:-}"' RETURN
|
||
printf '%s' "$ca" | base64 -d > "$temporary" 2>/dev/null \
|
||
|| die "Некорректный CA в control code"
|
||
openssl x509 -in "$temporary" -noout -checkend 86400 >/dev/null \
|
||
|| die "Control CA недействителен или скоро истекает"
|
||
download_control_layer
|
||
ensure_system_user "$AGENT_USER" "$AGENT_STATE"
|
||
install -d -m 0700 -o root -g root "$MESH_DIR"
|
||
ca_path="${AGENT_STATE}/control-ca.crt"
|
||
install -m 0600 -o "$AGENT_USER" -g "$AGENT_USER" "$temporary" "$ca_path"
|
||
configure_agent_service "$name" "$control_url"
|
||
runuser -u "$AGENT_USER" -- env \
|
||
"SMM_NodeId=${name}" \
|
||
"SMM_ControlUrl=${control_url}" \
|
||
"SMM_StateDirectory=${AGENT_STATE}" \
|
||
"SMM_CertificateAuthorityPath=${ca_path}" \
|
||
"SMM_EnrollToken=${token}" \
|
||
"$AGENT_BINARY"
|
||
systemctl daemon-reload
|
||
systemctl enable --now ochenstarik-smm-agent.service
|
||
trap - RETURN
|
||
rm -f -- "$temporary"
|
||
unset token payload ca CONTROL_JOIN_CODE SMM_CONTROL_CODE
|
||
log "Control Agent $name зарегистрирован и запущен"
|
||
}
|
||
|
||
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 'Вставьте одноразовый код SMM2: ' JOIN_CODE < /dev/tty
|
||
printf '\n'
|
||
else
|
||
die "Передайте код через SMM_JOIN_CODE"
|
||
fi
|
||
JOIN_CODE="${JOIN_CODE//$'\r'/}"
|
||
[[ "$JOIN_CODE" == SMM2-* ]] || die "Ожидается код формата SMM2-..."
|
||
}
|
||
|
||
install_node_mesh() {
|
||
local payload version name address token expires now private_key public_key
|
||
local hub_public_key endpoint allowed_ips request_payload request_code ack expected_ack tmp ssh_port
|
||
install_mesh_dependencies
|
||
read_join_code
|
||
payload="$(base64url_decode "${JOIN_CODE#SMM2-}")" || die "Не удалось декодировать код"
|
||
version="$(payload_value "$payload" VERSION)"
|
||
name="$(payload_value "$payload" NAME)"
|
||
address="$(payload_value "$payload" ADDRESS)"
|
||
token="$(payload_value "$payload" TOKEN)"
|
||
expires="$(payload_value "$payload" EXPIRES)"
|
||
hub_public_key="$(payload_value "$payload" HUB_PUBLIC_KEY)"
|
||
endpoint="$(payload_value "$payload" ENDPOINT)"
|
||
allowed_ips="$(payload_value "$payload" ALLOWED_IPS)"
|
||
[[ "$version" == 2 ]] || 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-подсеть"
|
||
[[ "$token" =~ ^[a-f0-9]{64}$ ]] || die "Некорректный enrollment token"
|
||
now="$(date +%s)"
|
||
[[ "$expires" =~ ^[0-9]+$ ]] && (( expires > now )) || die "Enrollment-код истёк"
|
||
[[ "$(printf '%s' "$hub_public_key" | base64 -d 2>/dev/null | wc -c)" -eq 32 ]] \
|
||
|| die "Некорректный публичный ключ Hub"
|
||
private_key="$(wg genkey)"
|
||
public_key="$(printf '%s' "$private_key" | wg pubkey)"
|
||
request_payload="$(printf 'VERSION=1\nTOKEN=%s\nNAME=%s\nADDRESS=%s\nPUBLIC_KEY=%s\n' \
|
||
"$token" "$name" "$address" "$public_key")"
|
||
request_code="SMMREQ1-$(printf '%s' "$request_payload" | base64url_encode)"
|
||
printf '\nПриватный WireGuard-ключ создан локально и не покидает Node.\n'
|
||
printf 'Request-код:\n\n%s\n\n' "$request_code"
|
||
printf 'На Hub выполните: sudo ochenstarik-smm node-enroll\n'
|
||
printf 'Вставьте request-код по скрытому запросу и верните полученный SMMACK1.\n\n'
|
||
expected_ack="$(printf '%s|%s|%s|%s|%s|%s|%s' \
|
||
"$token" "$public_key" "$name" "$address" "$hub_public_key" "$endpoint" "$allowed_ips" \
|
||
| sha256sum | awk '{ print $1 }')"
|
||
if [[ -n "${SMM_ENROLL_ACK:-}" ]]; then
|
||
ack="$SMM_ENROLL_ACK"
|
||
ack="${ack//$'\r'/}"
|
||
[[ "$ack" == "SMMACK1-${expected_ack}" ]] \
|
||
|| die "Hub не подтвердил регистрацию этого Node"
|
||
elif [[ -r /dev/tty ]]; then
|
||
while true; do
|
||
IFS= read -r -s -p 'Вставьте ответ SMMACK1 от Hub: ' ack < /dev/tty
|
||
printf '\n'
|
||
ack="${ack//$'\r'/}"
|
||
[[ "$ack" == "SMMACK1-${expected_ack}" ]] && break
|
||
warn "Код не подтверждает этот Node. Повторите вставку или нажмите Ctrl+C."
|
||
done
|
||
else
|
||
die "Передайте подтверждение через SMM_ENROLL_ACK"
|
||
fi
|
||
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" "$public_key" > "$MESH_DIR/node.conf"
|
||
chmod 0600 "$MESH_DIR/node.conf"
|
||
unset private_key token payload request_payload JOIN_CODE SMM_JOIN_CODE SMM_ENROLL_ACK ack expected_ack
|
||
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 "Вторичный сервер установлен"
|
||
log "После установки Control Hub добавьте Agent: sudo $0 install-control-agent"
|
||
}
|
||
|
||
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 'SSH-порт: %s\n' "${ssh_port:-unknown}"
|
||
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
|
||
warn "Серверная часть ещё не установлена"
|
||
fi
|
||
printf 'Control Hub: %s\n' "$(systemctl is-active ochenstarik-smm-control.service 2>/dev/null || true)"
|
||
printf 'Control Agent: %s\n' "$(systemctl is-active ochenstarik-smm-agent.service 2>/dev/null || true)"
|
||
}
|
||
|
||
install_server_part() {
|
||
install_monitoring
|
||
log "Серверная часть мониторинга установлена"
|
||
log "Входящий порт не изменялся, новые правила UFW не создавались"
|
||
show_status
|
||
}
|
||
|
||
confirm_action() {
|
||
local prompt="$1" answer
|
||
if [[ -r /dev/tty ]]; then
|
||
IFS= read -r -p "$prompt [y/N]: " answer < /dev/tty
|
||
else
|
||
die "Для этой операции требуется интерактивный терминал"
|
||
fi
|
||
[[ "$answer" =~ ^[Yy]$ ]]
|
||
}
|
||
|
||
backup_state() {
|
||
local backup_dir="/var/backups/${APP_NAME}" backup_file timestamp path
|
||
local -a paths=()
|
||
install -d -m 0700 -o root -g root "$backup_dir"
|
||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||
backup_file="${backup_dir}/${timestamp}.tar.gz"
|
||
for path in \
|
||
"$MESH_DIR" "$MONITOR_HOME" "$MONITOR_COMMAND" "$MONITOR_SSH_CONFIG" "$HUB_HELPER" "$HUB_CLI" \
|
||
"$CONTROL_POLICY_HELPER" "$WG_CONFIG" \
|
||
/etc/systemd/system/ochenstarik-smm-firewall.service \
|
||
/etc/systemd/system/ochenstarik-smm-firewall.timer \
|
||
/etc/sudoers.d/ochenstarik-smm-hub \
|
||
/etc/sysctl.d/90-ochenstarik-smm-forward.conf \
|
||
"$CONTROL_SERVICE" "$AGENT_SERVICE" "$CONTROL_BINARY" "$AGENT_BINARY" \
|
||
"$CONTROL_STATE" "$AGENT_STATE" "$CONTROL_ENV" "$AGENT_ENV" "$CONTROL_CA_CERT"; do
|
||
[[ -e "$path" || -L "$path" ]] && paths+=("${path#/}")
|
||
done
|
||
((${#paths[@]} > 0)) || { warn "Нет установленных файлов для backup"; return 0; }
|
||
tar -C / -czf "$backup_file" -- "${paths[@]}"
|
||
chmod 0600 "$backup_file"
|
||
log "Backup: $backup_file"
|
||
}
|
||
|
||
rollback_state() {
|
||
local backup_dir="/var/backups/${APP_NAME}" latest
|
||
latest="$(find "$backup_dir" -maxdepth 1 -type f -name '*.tar.gz' -printf '%T@ %p\n' 2>/dev/null \
|
||
| sort -nr | awk 'NR == 1 { sub(/^[^ ]+ /, ""); print }')"
|
||
[[ -n "$latest" && -f "$latest" ]] || die "Backup для rollback не найден"
|
||
confirm_action "Восстановить $latest" || { log "Отменено"; return 0; }
|
||
tar -C / -xzf "$latest"
|
||
systemctl daemon-reload
|
||
[[ ! -f "$WG_CONFIG" ]] || systemctl restart "wg-quick@${WG_INTERFACE}.service"
|
||
[[ ! -x "$HUB_HELPER" ]] || "$HUB_HELPER" firewall-restore
|
||
[[ ! -x "$MONITOR_COMMAND" ]] || systemctl reload ssh
|
||
log "Rollback завершён: $latest"
|
||
}
|
||
|
||
existing_public_key() {
|
||
[[ -r "$AUTHORIZED_KEYS" ]] || die "Не найден существующий SSH-ключ мониторинга"
|
||
sed -n 's/^.*\(ssh-ed25519 [A-Za-z0-9+\/=]*.*\)$/\1/p' "$AUTHORIZED_KEYS" | head -n 1
|
||
}
|
||
|
||
update_installed() {
|
||
local role=monitor
|
||
backup_state
|
||
SERVER_MONITOR_PUBLIC_KEY="$(existing_public_key)"
|
||
export SERVER_MONITOR_PUBLIC_KEY
|
||
if [[ -r "$HUB_CONFIG" ]]; then
|
||
role=hub
|
||
SMM_HUB_ENDPOINT="$(awk -F= '$1 == "HUB_ENDPOINT" { print $2; exit }' "$HUB_CONFIG")"
|
||
SMM_WG_PORT="$(awk -F= '$1 == "WG_PORT" { print $2; exit }' "$HUB_CONFIG")"
|
||
export SMM_HUB_ENDPOINT SMM_WG_PORT
|
||
install_hub
|
||
elif [[ -r "$MESH_DIR/node.conf" ]]; then
|
||
role=node
|
||
install_monitoring
|
||
install_mesh_dependencies
|
||
systemctl enable --now "wg-quick@${WG_INTERFACE}.service"
|
||
systemctl restart "wg-quick@${WG_INTERFACE}.service"
|
||
else
|
||
install_server_part
|
||
fi
|
||
unset SERVER_MONITOR_PUBLIC_KEY SMM_HUB_ENDPOINT SMM_WG_PORT
|
||
log "Обновление роли $role завершено"
|
||
}
|
||
|
||
uninstall_monitor() {
|
||
[[ ! -r "$HUB_CONFIG" && ! -r "$MESH_DIR/node.conf" ]] \
|
||
|| die "Сначала удалите роль командой uninstall-hub или uninstall-node"
|
||
confirm_action "Удалить пользователя мониторинга и forced-command" || { log "Отменено"; return 0; }
|
||
backup_state
|
||
rm -f -- "$MONITOR_COMMAND" "$MONITOR_SSH_CONFIG"
|
||
if getent passwd "$MONITOR_USER" >/dev/null; then
|
||
userdel --remove "$MONITOR_USER" 2>/dev/null || userdel "$MONITOR_USER"
|
||
fi
|
||
systemctl reload ssh
|
||
log "Серверная часть мониторинга удалена"
|
||
}
|
||
|
||
uninstall_node() {
|
||
local ssh_port
|
||
[[ -r "$MESH_DIR/node.conf" ]] || die "Роль Node не установлена"
|
||
confirm_action "Отключить и удалить WireGuard Node" || { log "Отменено"; return 0; }
|
||
backup_state
|
||
systemctl disable --now "wg-quick@${WG_INTERFACE}.service" 2>/dev/null || true
|
||
systemctl disable --now ochenstarik-smm-agent.service 2>/dev/null || true
|
||
ssh_port="$(sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }')"
|
||
if command -v ufw >/dev/null 2>&1 && [[ -n "$ssh_port" ]]; then
|
||
ufw --force delete allow in on "$WG_INTERFACE" to any port "$ssh_port" proto tcp >/dev/null 2>&1 || true
|
||
fi
|
||
rm -f -- "$WG_CONFIG" "$MESH_DIR/node.conf" "$AGENT_SERVICE" "$AGENT_ENV"
|
||
rm -rf -- "$AGENT_STATE" "$(dirname "$AGENT_BINARY")"
|
||
getent passwd "$AGENT_USER" >/dev/null && userdel "$AGENT_USER" 2>/dev/null || true
|
||
systemctl daemon-reload
|
||
rmdir "$MESH_DIR" 2>/dev/null || true
|
||
log "WireGuard Node удалён; monitoring identity оставлена"
|
||
}
|
||
|
||
uninstall_hub() {
|
||
local wg_port
|
||
[[ -r "$HUB_CONFIG" ]] || die "Роль Hub не установлена"
|
||
confirm_action "Удалить Hub, все Node identities, Links и аудит" || { log "Отменено"; return 0; }
|
||
backup_state
|
||
wg_port="$(awk -F= '$1 == "WG_PORT" { print $2; exit }' "$HUB_CONFIG")"
|
||
systemctl disable --now ochenstarik-smm-firewall.timer ochenstarik-smm-firewall.service 2>/dev/null || true
|
||
systemctl disable --now ochenstarik-smm-control.service 2>/dev/null || true
|
||
systemctl disable --now "wg-quick@${WG_INTERFACE}.service" 2>/dev/null || true
|
||
nft delete table inet ochenstarik_smm >/dev/null 2>&1 || true
|
||
if command -v ufw >/dev/null 2>&1; then
|
||
ufw --force delete allow "$wg_port/udp" >/dev/null 2>&1 || true
|
||
ufw --force route delete allow in on "$WG_INTERFACE" out on "$WG_INTERFACE" >/dev/null 2>&1 || true
|
||
fi
|
||
rm -f -- \
|
||
/etc/systemd/system/ochenstarik-smm-firewall.service \
|
||
/etc/systemd/system/ochenstarik-smm-firewall.timer \
|
||
/etc/sudoers.d/ochenstarik-smm-hub \
|
||
/etc/sysctl.d/90-ochenstarik-smm-forward.conf \
|
||
"$HUB_HELPER" "$HUB_CLI" "$CONTROL_POLICY_HELPER" "$WG_CONFIG" \
|
||
"$CONTROL_SERVICE" "$CONTROL_ENV" "$CONTROL_CA_CERT"
|
||
rm -rf -- "$CONTROL_STATE" "$(dirname "$CONTROL_BINARY")"
|
||
getent passwd "$CONTROL_USER" >/dev/null && userdel "$CONTROL_USER" 2>/dev/null || true
|
||
rm -rf -- "$MESH_DIR"
|
||
systemctl daemon-reload
|
||
sysctl --system >/dev/null
|
||
log "Mesh Hub удалён; monitoring identity оставлена"
|
||
}
|
||
|
||
main() {
|
||
local action="${1:-install}"
|
||
require_root
|
||
detect_system
|
||
case "$action" in
|
||
install|install-monitor) install_server_part ;;
|
||
hub|install-hub) install_hub ;;
|
||
node|install-node) install_node ;;
|
||
install-control-hub) install_control_hub ;;
|
||
install-control-agent) install_control_agent ;;
|
||
control-code) [[ $# -eq 2 ]] || die "Использование: $0 control-code NAME"; create_control_join_code "$2" ;;
|
||
control-device-code) [[ $# -eq 2 ]] || die "Использование: $0 control-device-code DEVICE"; create_device_join_code "$2" ;;
|
||
hub-code) [[ $# -eq 2 ]] || die "Использование: $0 hub-code NAME"; exec "$HUB_HELPER" node-code "$2" ;;
|
||
status) show_status ;;
|
||
update) update_installed ;;
|
||
rollback) rollback_state ;;
|
||
uninstall-monitor) uninstall_monitor ;;
|
||
uninstall-node) uninstall_node ;;
|
||
uninstall-hub) uninstall_hub ;;
|
||
uninstall) die "Укажите роль: uninstall-monitor, uninstall-node или uninstall-hub" ;;
|
||
*) die "Использование: $0 {install-monitor|install-hub|install-node|install-control-hub|install-control-agent|control-code NAME|control-device-code DEVICE|hub-code NAME|status|update|rollback|uninstall-monitor|uninstall-node|uninstall-hub}" ;;
|
||
esac
|
||
}
|
||
|
||
main "$@"
|