mnogoweb revised this gist 23 hours ago. Go to revision
No changes
mnogoweb revised this gist 23 hours ago. Go to revision
1 file changed, 458 insertions
vgwm(file created)
| @@ -0,0 +1,458 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # vgwm — VersityGW Manager (AlmaLinux 8/9) | |
| 3 | + | # | |
| 4 | + | # vgwm install [--template FILE] [--rpm FILE] полная установка | |
| 5 | + | # vgwm firewall только firewall на nftables (без VersityGW): сброс всех | |
| 6 | + | # правил и создание заново в интерактиве | |
| 7 | + | # vgwm update обновление VersityGW из GitHub Releases | |
| 8 | + | # vgwm self-install только установить vgwm в /usr/local/bin | |
| 9 | + | # | |
| 10 | + | # --template FILE — файл-образец конфига: берутся только активные (не закомментированные) | |
| 11 | + | # строки, серверо-зависимые параметры запрашиваются интерактивно. | |
| 12 | + | # --rpm FILE — локальный RPM вместо загрузки последнего релиза. | |
| 13 | + | # --debug — подробный вывод (например, загруженные правила nftables). | |
| 14 | + | ||
| 15 | + | set -Eeuo pipefail | |
| 16 | + | ||
| 17 | + | INSTANCE="mnogoweb" | |
| 18 | + | CONF="/etc/versitygw.d/${INSTANCE}.conf" | |
| 19 | + | SERVICE="versitygw@${INSTANCE}" | |
| 20 | + | STATE_DIR="/etc/vgwm" | |
| 21 | + | CLIENTS_FILE="${STATE_DIR}/clients.list" | |
| 22 | + | GH_API="https://api.github.com/repos/versity/versitygw/releases/latest" | |
| 23 | + | ||
| 24 | + | ADMIN_IPS=(94.250.251.190 46.161.40.8) # SSH + 80/443 | |
| 25 | + | MON_IPS=(212.237.218.213 37.228.116.152) # Zabbix + Beszel | |
| 26 | + | SSH_PORT=9999 | |
| 27 | + | ZABBIX_PORT=10050 | |
| 28 | + | BESZEL_PORT=45876 | |
| 29 | + | ||
| 30 | + | # ---------- helpers ---------- | |
| 31 | + | log() { echo -e "\e[32m[+]\e[0m $*"; } | |
| 32 | + | warn() { echo -e "\e[33m[!]\e[0m $*" >&2; } | |
| 33 | + | die() { echo -e "\e[31m[x]\e[0m $*" >&2; exit 1; } | |
| 34 | + | ||
| 35 | + | ask() { # ask "Вопрос" "default" -> stdout | |
| 36 | + | local prompt="$1" def="${2:-}" ans | |
| 37 | + | if [[ -n "$def" ]]; then read -rp "$prompt [$def]: " ans; echo "${ans:-$def}" | |
| 38 | + | else read -rp "$prompt: " ans; echo "$ans"; fi | |
| 39 | + | } | |
| 40 | + | ask_secret() { | |
| 41 | + | local prompt="$1" def="${2:-}" ans | |
| 42 | + | read -rsp "$prompt${def:+ [оставить текущий]}: " ans; echo >&2 | |
| 43 | + | echo "${ans:-$def}" | |
| 44 | + | } | |
| 45 | + | yes_no() { local a; read -rp "$1 [y/N]: " a; [[ "$a" =~ ^[YyДд]$ ]]; } | |
| 46 | + | ||
| 47 | + | valid_ip() { # IPv4/IPv6 с опциональным /маска | |
| 48 | + | python3 - "$1" <<'PY' 2>/dev/null | |
| 49 | + | import ipaddress, sys | |
| 50 | + | ipaddress.ip_network(sys.argv[1], strict=False) | |
| 51 | + | PY | |
| 52 | + | } | |
| 53 | + | ip_family() { [[ "$1" == *:* ]] && echo ipv6 || echo ipv4; } | |
| 54 | + | ||
| 55 | + | need_root() { [[ $EUID -eq 0 ]] || die "Запускать от root"; } | |
| 56 | + | ||
| 57 | + | # ---------- RPM ---------- | |
| 58 | + | rpm_arch_regex() { | |
| 59 | + | case "$(uname -m)" in | |
| 60 | + | x86_64) echo 'x86_64|amd64' ;; | |
| 61 | + | aarch64) echo 'aarch64|arm64' ;; | |
| 62 | + | *) die "Неподдерживаемая архитектура: $(uname -m)" ;; | |
| 63 | + | esac | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | download_latest_rpm() { # -> путь к rpm в stdout | |
| 67 | + | local json url tag tmp | |
| 68 | + | command -v curl >/dev/null || dnf -y install curl >&2 | |
| 69 | + | json="$(curl -fsSL "$GH_API")" || die "Не удалось получить данные релиза с GitHub" | |
| 70 | + | tag="$(grep -oP '"tag_name":\s*"\K[^"]+' <<<"$json" | head -1)" | |
| 71 | + | url="$(grep -oP '"browser_download_url":\s*"\K[^"]+\.rpm' <<<"$json" \ | |
| 72 | + | | grep -Ei "$(rpm_arch_regex)" | head -1)" | |
| 73 | + | [[ -n "$url" ]] || die "В релизе $tag не найден RPM для $(uname -m)" | |
| 74 | + | tmp="$(mktemp -d)" | |
| 75 | + | log "Скачиваю $tag: $url" >&2 | |
| 76 | + | curl -fL --retry 3 -o "$tmp/$(basename "$url")" "$url" >&2 | |
| 77 | + | echo "$tmp/$(basename "$url")" | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | install_rpm() { | |
| 81 | + | local rpm="$1" | |
| 82 | + | [[ -f "$rpm" ]] || die "RPM не найден: $rpm" | |
| 83 | + | dnf -y install "$rpm" | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | # ---------- config ---------- | |
| 87 | + | set_kv() { # set_kv KEY VALUE FILE — заменить активный ключ или дописать | |
| 88 | + | local k="$1" v="$2" f="$3" | |
| 89 | + | if grep -qE "^[[:space:]]*${k}=" "$f"; then | |
| 90 | + | sed -i -E "s|^[[:space:]]*${k}=.*|${k}=${v//|/\\|}|" "$f" | |
| 91 | + | else | |
| 92 | + | echo "${k}=${v}" >>"$f" | |
| 93 | + | fi | |
| 94 | + | } | |
| 95 | + | get_kv() { grep -E "^[[:space:]]*$1=" "$2" 2>/dev/null | tail -1 | cut -d= -f2- | tr -d '"' || true; } | |
| 96 | + | ||
| 97 | + | # Активные параметры эталонного шаблона mnogoweb. | |
| 98 | + | # __REGION__, ключи и пути подставляются при установке. | |
| 99 | + | default_template() { | |
| 100 | + | cat <<'EOF' | |
| 101 | + | VGW_BACKEND=posix | |
| 102 | + | VGW_BACKEND_ARG=/data/s3 | |
| 103 | + | ROOT_ACCESS_KEY_ID= | |
| 104 | + | ROOT_SECRET_ACCESS_KEY= | |
| 105 | + | VGW_PORT=:443 | |
| 106 | + | VGW_REGION=__REGION__ | |
| 107 | + | VGW_CERT=/etc/versitygw.d/ssl.cert | |
| 108 | + | VGW_KEY=/etc/versitygw.d/ssl.key | |
| 109 | + | VGW_VIRTUAL_DOMAIN=s3.bs-__REGION__.mnogoweb.com | |
| 110 | + | VGW_DISABLE_STRICT_BUCKET_NAMES=true | |
| 111 | + | VGW_MP_MAX_PARTS=100000 | |
| 112 | + | VGW_COPY_OBJECT_THRESHOLD=64424509440 | |
| 113 | + | VGW_WEBUI_S3_PREFIX=/ui | |
| 114 | + | VGW_WEBUI_CERT=/etc/versitygw.d/ssl.cert | |
| 115 | + | VGW_WEBUI_KEY=/etc/versitygw.d/ssl.key | |
| 116 | + | VGW_CORS_ALLOW_ORIGIN=https://bs-__REGION__.mnogoweb.com | |
| 117 | + | VGW_WEBUI_GATEWAYS=https://s3.bs-__REGION__.mnogoweb.com | |
| 118 | + | VGW_DEBUG=true | |
| 119 | + | EOF | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | gen_key() { python3 -c "import secrets,string,sys;a=string.ascii_letters+string.digits;print(''.join(secrets.choice(a) for _ in range(int(sys.argv[1]))))" "$1"; } | |
| 123 | + | ||
| 124 | + | place_tls() { # place_tls DEST "описание" — копирует файл в DEST, если его ещё нет | |
| 125 | + | local dest="$1" what="$2" src | |
| 126 | + | if [[ -f "$dest" ]]; then | |
| 127 | + | src="$(ask "$what уже есть ($dest). Путь к новому файлу (пусто — оставить)" "")" | |
| 128 | + | else | |
| 129 | + | src="$(ask "Путь к файлу: $what (будет скопирован в $dest)" "")" | |
| 130 | + | fi | |
| 131 | + | if [[ -n "$src" ]]; then | |
| 132 | + | [[ -f "$src" ]] || die "Файл не найден: $src" | |
| 133 | + | install -m 0640 -o root -g versitygw "$src" "$dest" | |
| 134 | + | fi | |
| 135 | + | [[ -f "$dest" ]] || die "$what отсутствует: $dest — без него сервис не запустится" | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | configure_vgw() { | |
| 139 | + | local template="${1:-}" tmp | |
| 140 | + | tmp="$(mktemp)" | |
| 141 | + | if [[ -n "$template" ]]; then | |
| 142 | + | [[ -f "$template" ]] || die "Шаблон не найден: $template" | |
| 143 | + | # только активные строки: без комментариев и пустых | |
| 144 | + | grep -Ev '^[[:space:]]*(#|$)' "$template" | sed -E 's/[[:space:]]+$//' >"$tmp" | |
| 145 | + | log "Из шаблона взято активных параметров: $(wc -l <"$tmp")" | |
| 146 | + | else | |
| 147 | + | default_template >"$tmp" | |
| 148 | + | fi | |
| 149 | + | ||
| 150 | + | # значения уже существующего конфига — как дефолты при повторном запуске | |
| 151 | + | local old="$CONF"; [[ -f "$old" ]] || old=/dev/null | |
| 152 | + | ||
| 153 | + | echo "--- Серверо-зависимые параметры ---" | |
| 154 | + | local region domain backend_arg access secret def_reg cert key iam | |
| 155 | + | def_reg="$(get_kv VGW_REGION "$old")"; def_reg="${def_reg:-$(get_kv VGW_REGION "$tmp")}" | |
| 156 | + | [[ "$def_reg" == "__REGION__" ]] && def_reg="" | |
| 157 | + | region="$(ask "Регион (VGW_REGION), напр. us1" "$def_reg")" | |
| 158 | + | [[ "$region" =~ ^[a-z0-9-]+$ ]] || die "Некорректный регион: $region" | |
| 159 | + | domain="$(ask "Базовый домен" "bs-${region}.mnogoweb.com")" | |
| 160 | + | ||
| 161 | + | backend_arg="$(get_kv VGW_BACKEND_ARG "$old")"; backend_arg="${backend_arg:-$(get_kv VGW_BACKEND_ARG "$tmp")}" | |
| 162 | + | backend_arg="$(ask "Каталог данных (VGW_BACKEND_ARG)" "$backend_arg")" | |
| 163 | + | ||
| 164 | + | access="$(get_kv ROOT_ACCESS_KEY_ID "$old")" | |
| 165 | + | secret="$(get_kv ROOT_SECRET_ACCESS_KEY "$old")" | |
| 166 | + | if [[ -z "$access" || -z "$secret" ]] || yes_no "Сгенерировать новые root-ключи?"; then | |
| 167 | + | if yes_no "Ввести root-ключи вручную (иначе — сгенерировать)?"; then | |
| 168 | + | access="$(ask "Root access key" "")" | |
| 169 | + | secret="$(ask_secret "Root secret key" "")" | |
| 170 | + | else | |
| 171 | + | access="$(gen_key 16)"; secret="$(gen_key 32)" | |
| 172 | + | fi | |
| 173 | + | fi | |
| 174 | + | [[ -n "$access" && -n "$secret" ]] || die "Access/secret обязательны" | |
| 175 | + | ||
| 176 | + | sed -i "s/__REGION__/${region}/g" "$tmp" | |
| 177 | + | set_kv VGW_REGION "$region" "$tmp" | |
| 178 | + | set_kv VGW_VIRTUAL_DOMAIN "s3.${domain}" "$tmp" | |
| 179 | + | set_kv VGW_CORS_ALLOW_ORIGIN "https://${domain}" "$tmp" | |
| 180 | + | set_kv VGW_WEBUI_GATEWAYS "https://s3.${domain}" "$tmp" | |
| 181 | + | set_kv VGW_BACKEND_ARG "$backend_arg" "$tmp" | |
| 182 | + | set_kv ROOT_ACCESS_KEY_ID "$access" "$tmp" | |
| 183 | + | set_kv ROOT_SECRET_ACCESS_KEY "$secret" "$tmp" | |
| 184 | + | ||
| 185 | + | mkdir -p /etc/versitygw.d | |
| 186 | + | [[ -f "$CONF" ]] && cp -a "$CONF" "${CONF}.bak.$(date +%F_%H%M%S)" | |
| 187 | + | install -m 0640 -o root -g versitygw "$tmp" "$CONF" | |
| 188 | + | rm -f "$tmp" | |
| 189 | + | ||
| 190 | + | # TLS: сертификат для s3.${domain} и *.s3.${domain} (virtual-host бакеты) | |
| 191 | + | cert="$(get_kv VGW_CERT "$CONF")"; key="$(get_kv VGW_KEY "$CONF")" | |
| 192 | + | [[ -n "$cert" ]] && place_tls "$cert" "TLS-сертификат (fullchain)" | |
| 193 | + | [[ -n "$key" ]] && place_tls "$key" "TLS-ключ" | |
| 194 | + | ||
| 195 | + | # каталоги и права | |
| 196 | + | mkdir -p "$backend_arg"; chown versitygw:versitygw "$backend_arg" | |
| 197 | + | iam="$(get_kv VGW_IAM_DIR "$CONF")" | |
| 198 | + | [[ -n "$iam" ]] && { mkdir -p "$iam"; chown versitygw:versitygw "$iam"; } | |
| 199 | + | ||
| 200 | + | log "Конфиг записан: $CONF" | |
| 201 | + | echo " Регион: $region" | |
| 202 | + | echo " S3: https://s3.${domain} (WebUI: https://s3.${domain}/ui)" | |
| 203 | + | echo " Access key: $access" | |
| 204 | + | echo " Secret key: $secret <- сохраните" | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | enable_service() { | |
| 208 | + | # разрешаем непривилегированному пользователю слушать 80/443 | |
| 209 | + | mkdir -p "/etc/systemd/system/versitygw@.service.d" | |
| 210 | + | cat >/etc/systemd/system/versitygw@.service.d/override.conf <<'EOF' | |
| 211 | + | [Service] | |
| 212 | + | AmbientCapabilities=CAP_NET_BIND_SERVICE | |
| 213 | + | CapabilityBoundingSet=CAP_NET_BIND_SERVICE | |
| 214 | + | EOF | |
| 215 | + | systemctl daemon-reload | |
| 216 | + | systemctl enable "$SERVICE" | |
| 217 | + | systemctl restart "$SERVICE" | |
| 218 | + | sleep 2 | |
| 219 | + | systemctl is-active --quiet "$SERVICE" \ | |
| 220 | + | && log "Сервис $SERVICE запущен" \ | |
| 221 | + | || { journalctl -u "$SERVICE" -n 30 --no-pager; die "Сервис не стартовал"; } | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | # ---------- firewall (nftables) ---------- | |
| 225 | + | read_ip_list() { # интерактивный ввод списка IP -> CLIENTS[] | |
| 226 | + | local def="" input | |
| 227 | + | [[ -f "$CLIENTS_FILE" ]] && def="$(tr '\n' ' ' <"$CLIENTS_FILE" | sed 's/ $//')" | |
| 228 | + | echo "Список IP/подсетей клиентов для 80/443 и FTP (через пробел или запятую)." | |
| 229 | + | input="$(ask "IP клиентов" "$def")" | |
| 230 | + | CLIENTS=() | |
| 231 | + | for ip in ${input//,/ }; do | |
| 232 | + | valid_ip "$ip" && CLIENTS+=("$ip") || warn "Пропущен некорректный адрес: $ip" | |
| 233 | + | done | |
| 234 | + | mkdir -p "$STATE_DIR" | |
| 235 | + | printf '%s\n' "${CLIENTS[@]}" >"$CLIENTS_FILE" | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | NFT_RULES="/etc/nftables/vgwm.nft" | |
| 239 | + | NFT_MAIN="/etc/sysconfig/nftables.conf" | |
| 240 | + | ROLLBACK_SEC=60 | |
| 241 | + | ||
| 242 | + | # элементы set'а для одного семейства: nft_elems ipv4|ipv6 IP... | |
| 243 | + | nft_elems() { | |
| 244 | + | local fam="$1"; shift | |
| 245 | + | local out=() ip | |
| 246 | + | for ip in "$@"; do [[ "$(ip_family "$ip")" == "$fam" ]] && out+=("$ip"); done | |
| 247 | + | (( ${#out[@]} )) && { local IFS=,; echo "elements = { ${out[*]} };"; } | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | gen_ruleset() { # gen_ruleset PASV -> stdout | |
| 251 | + | local pasv="$1" | |
| 252 | + | cat <<EOF | |
| 253 | + | #!/usr/sbin/nft -f | |
| 254 | + | # Сгенерировано vgwm $(date '+%F %T'). Ручные правки будут перезаписаны. | |
| 255 | + | flush ruleset | |
| 256 | + | ||
| 257 | + | table inet vgwm { | |
| 258 | + | set admin4 { type ipv4_addr; flags interval; $(nft_elems ipv4 "${ADMIN_IPS[@]}") } | |
| 259 | + | set admin6 { type ipv6_addr; flags interval; $(nft_elems ipv6 "${ADMIN_IPS[@]}") } | |
| 260 | + | set clients4 { type ipv4_addr; flags interval; $(nft_elems ipv4 "${CLIENTS[@]}") } | |
| 261 | + | set clients6 { type ipv6_addr; flags interval; $(nft_elems ipv6 "${CLIENTS[@]}") } | |
| 262 | + | set mon4 { type ipv4_addr; flags interval; $(nft_elems ipv4 "${MON_IPS[@]}") } | |
| 263 | + | set mon6 { type ipv6_addr; flags interval; $(nft_elems ipv6 "${MON_IPS[@]}") } | |
| 264 | + | ||
| 265 | + | chain input { | |
| 266 | + | type filter hook input priority filter; policy drop; | |
| 267 | + | ||
| 268 | + | iif lo accept | |
| 269 | + | ct state established,related accept | |
| 270 | + | ct state invalid drop | |
| 271 | + | ||
| 272 | + | # IPv6 не работает без Neighbor Discovery | |
| 273 | + | icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert, nd-router-solicit } ip6 hoplimit 255 accept | |
| 274 | + | ||
| 275 | + | # SSH | |
| 276 | + | tcp dport $SSH_PORT ip saddr @admin4 accept | |
| 277 | + | tcp dport $SSH_PORT ip6 saddr @admin6 accept | |
| 278 | + | ||
| 279 | + | # HTTP/HTTPS: админы + клиенты | |
| 280 | + | tcp dport { 80, 443 } ip saddr @admin4 accept | |
| 281 | + | tcp dport { 80, 443 } ip6 saddr @admin6 accept | |
| 282 | + | tcp dport { 80, 443 } ip saddr @clients4 accept | |
| 283 | + | tcp dport { 80, 443 } ip6 saddr @clients6 accept | |
| 284 | + | ||
| 285 | + | # FTP (control + passive) | |
| 286 | + | tcp dport { 21, $pasv } ip saddr @clients4 accept | |
| 287 | + | tcp dport { 21, $pasv } ip6 saddr @clients6 accept | |
| 288 | + | ||
| 289 | + | # Zabbix-agent, Beszel-agent | |
| 290 | + | tcp dport { $ZABBIX_PORT, $BESZEL_PORT } ip saddr @mon4 accept | |
| 291 | + | tcp dport { $ZABBIX_PORT, $BESZEL_PORT } ip6 saddr @mon6 accept | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | chain forward { | |
| 295 | + | type filter hook forward priority filter; policy drop; | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | chain output { | |
| 299 | + | type filter hook output priority filter; policy accept; | |
| 300 | + | } | |
| 301 | + | } | |
| 302 | + | EOF | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | ensure_nftables() { | |
| 306 | + | rpm -q nftables >/dev/null 2>&1 || dnf -y install nftables || die "Не удалось установить nftables" | |
| 307 | + | ||
| 308 | + | # всё, что тоже управляет netfilter, должно быть выключено | |
| 309 | + | local svc | |
| 310 | + | for svc in firewalld iptables ip6tables ufw; do | |
| 311 | + | if systemctl is-active --quiet "$svc" 2>/dev/null || systemctl is-enabled --quiet "$svc" 2>/dev/null; then | |
| 312 | + | warn "Найден конкурирующий сервис: $svc" | |
| 313 | + | yes_no "Остановить, отключить и замаскировать $svc?" || die "Конфликт с $svc — прервано" | |
| 314 | + | systemctl disable --now "$svc" >/dev/null 2>&1 || true | |
| 315 | + | systemctl mask "$svc" >/dev/null 2>&1 || true | |
| 316 | + | fi | |
| 317 | + | done | |
| 318 | + | ||
| 319 | + | [[ "$(systemctl is-enabled nftables 2>/dev/null)" == "masked" ]] && systemctl unmask nftables | |
| 320 | + | command -v nft >/dev/null || die "nft не найден" | |
| 321 | + | log "nftables $(nft --version | awk '{print $2}')" | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | apply_with_rollback() { # apply_with_rollback FILE | |
| 325 | + | local new="$1" backup | |
| 326 | + | backup="$(mktemp)" | |
| 327 | + | nft list ruleset >"$backup" | |
| 328 | + | ||
| 329 | + | nft -c -f "$new" || { rm -f "$backup"; die "Ошибка синтаксиса правил: $new"; } | |
| 330 | + | nft -f "$new" || { rm -f "$backup"; die "Не удалось применить правила"; } | |
| 331 | + | ||
| 332 | + | if [[ -t 0 && -n "${SSH_CONNECTION:-}" ]]; then | |
| 333 | + | warn "Правила применены. Откройте НОВУЮ SSH-сессию и проверьте доступ." | |
| 334 | + | local ans="" | |
| 335 | + | read -r -t "$ROLLBACK_SEC" -p "Введите 'ok' в течение ${ROLLBACK_SEC} c, иначе откат: " ans || true | |
| 336 | + | echo | |
| 337 | + | if [[ "$ans" != "ok" ]]; then | |
| 338 | + | { echo "flush ruleset"; cat "$backup"; } | nft -f - | |
| 339 | + | rm -f "$backup" | |
| 340 | + | die "Подтверждения нет — правила откатаны, изменения не сохранены" | |
| 341 | + | fi | |
| 342 | + | fi | |
| 343 | + | rm -f "$backup" | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | persist_ruleset() { | |
| 347 | + | install -d -m 0755 "$(dirname "$NFT_RULES")" | |
| 348 | + | install -m 0600 "$1" "$NFT_RULES" | |
| 349 | + | [[ -f "$NFT_MAIN" ]] && ! grep -qx "include \"$NFT_RULES\"" "$NFT_MAIN" \ | |
| 350 | + | && cp -a "$NFT_MAIN" "${NFT_MAIN}.bak.$(date +%F_%H%M%S)" | |
| 351 | + | cat >"$NFT_MAIN" <<EOF | |
| 352 | + | # Управляется vgwm. Все правила — в $NFT_RULES | |
| 353 | + | include "$NFT_RULES" | |
| 354 | + | EOF | |
| 355 | + | systemctl enable nftables >/dev/null 2>&1 | |
| 356 | + | systemctl restart nftables || { journalctl -u nftables -n 20 --no-pager; die "nftables.service не стартовал"; } | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | configure_firewall() { | |
| 360 | + | ensure_nftables | |
| 361 | + | ||
| 362 | + | read_ip_list | |
| 363 | + | local pasv; pasv="$(ask "Диапазон пассивных портов FTP" "$(cat "$STATE_DIR/ftp_pasv" 2>/dev/null || echo 30000-31000)")" | |
| 364 | + | [[ "$pasv" =~ ^[0-9]+-[0-9]+$ ]] || die "Неверный диапазон: $pasv" | |
| 365 | + | echo "$pasv" >"$STATE_DIR/ftp_pasv" | |
| 366 | + | ||
| 367 | + | # защита от потери доступа | |
| 368 | + | local me="${SSH_CLIENT%% *}" | |
| 369 | + | if [[ -n "$me" ]] && ! printf '%s\n' "${ADMIN_IPS[@]}" | grep -qx "$me"; then | |
| 370 | + | warn "Ваш текущий IP $me НЕ входит в список SSH ($SSH_PORT): ${ADMIN_IPS[*]}" | |
| 371 | + | yes_no "Продолжить?" || die "Отменено" | |
| 372 | + | fi | |
| 373 | + | ss -ltnH "sport = :$SSH_PORT" 2>/dev/null | grep -q . \ | |
| 374 | + | || warn "Никто не слушает порт $SSH_PORT — после применения SSH будет недоступен" | |
| 375 | + | ||
| 376 | + | local tmp; tmp="$(mktemp)" | |
| 377 | + | gen_ruleset "$pasv" >"$tmp" | |
| 378 | + | ||
| 379 | + | log "Сброс всех правил и применение нового набора" | |
| 380 | + | apply_with_rollback "$tmp" | |
| 381 | + | persist_ruleset "$tmp" | |
| 382 | + | rm -f "$tmp" | |
| 383 | + | ||
| 384 | + | log "Firewall применён и сохранён в $NFT_RULES" | |
| 385 | + | if (( DEBUG )); then | |
| 386 | + | echo "--- [debug] загруженные правила ---" | |
| 387 | + | nft list table inet vgwm | |
| 388 | + | fi | |
| 389 | + | } | |
| 390 | + | ||
| 391 | + | # ---------- update ---------- | |
| 392 | + | update_vgw() { | |
| 393 | + | local cur rpm new | |
| 394 | + | cur="$(rpm -q versitygw 2>/dev/null || echo 'не установлен')" | |
| 395 | + | rpm="$(download_latest_rpm)" | |
| 396 | + | new="$(rpm -qp --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}' "$rpm")" | |
| 397 | + | log "Установлено: $cur | Доступно: $new" | |
| 398 | + | if [[ "$cur" == "$new" ]]; then log "Уже актуальная версия"; rm -rf "$(dirname "$rpm")"; return; fi | |
| 399 | + | dnf -y install "$rpm" | |
| 400 | + | rm -rf "$(dirname "$rpm")" | |
| 401 | + | systemctl daemon-reload | |
| 402 | + | systemctl restart "$SERVICE" | |
| 403 | + | systemctl is-active --quiet "$SERVICE" && log "Обновлено, сервис работает" \ | |
| 404 | + | || { journalctl -u "$SERVICE" -n 30 --no-pager; die "Сервис не стартовал после обновления"; } | |
| 405 | + | versitygw --version || true | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | # ---------- self-install ---------- | |
| 409 | + | VGWM_BIN="/usr/local/bin/vgwm" | |
| 410 | + | ||
| 411 | + | self_install() { | |
| 412 | + | local self; self="$(readlink -f "$0" 2>/dev/null || true)" | |
| 413 | + | [[ -f "$self" && "$self" != "$VGWM_BIN" ]] || return 0 # уже установлен или запуск через pipe | |
| 414 | + | if [[ -f "$VGWM_BIN" ]] && cmp -s "$self" "$VGWM_BIN"; then return 0; fi | |
| 415 | + | local action="Установлен"; [[ -f "$VGWM_BIN" ]] && action="Обновлён" | |
| 416 | + | install -m 0755 -o root -g root "$self" "$VGWM_BIN" | |
| 417 | + | log "$action vgwm → $VGWM_BIN (дальше можно запускать просто: vgwm <режим>)" | |
| 418 | + | } | |
| 419 | + | ||
| 420 | + | usage() { sed -n '2,14p' "$VGWM_BIN" 2>/dev/null || sed -n '2,14p' "$0"; } | |
| 421 | + | ||
| 422 | + | # ---------- main ---------- | |
| 423 | + | DEBUG=0 | |
| 424 | + | ||
| 425 | + | main() { | |
| 426 | + | # --debug допускается в любом месте командной строки | |
| 427 | + | local args=() a | |
| 428 | + | for a in "$@"; do [[ "$a" == "--debug" ]] && DEBUG=1 || args+=("$a"); done | |
| 429 | + | set -- "${args[@]+"${args[@]}"}" | |
| 430 | + | ||
| 431 | + | need_root | |
| 432 | + | self_install | |
| 433 | + | local mode="${1:-}"; shift || true | |
| 434 | + | local template="" rpm="" | |
| 435 | + | while [[ $# -gt 0 ]]; do | |
| 436 | + | case "$1" in | |
| 437 | + | --template) template="$2"; shift 2 ;; | |
| 438 | + | --rpm) rpm="$2"; shift 2 ;; | |
| 439 | + | *) die "Неизвестный параметр: $1" ;; | |
| 440 | + | esac | |
| 441 | + | done | |
| 442 | + | ||
| 443 | + | case "$mode" in | |
| 444 | + | install) | |
| 445 | + | [[ -n "$rpm" ]] || rpm="$(download_latest_rpm)" | |
| 446 | + | install_rpm "$rpm" | |
| 447 | + | configure_vgw "$template" | |
| 448 | + | enable_service | |
| 449 | + | configure_firewall | |
| 450 | + | ;; | |
| 451 | + | firewall|firewall-only|fw) configure_firewall ;; | |
| 452 | + | update) update_vgw ;; | |
| 453 | + | self-install) log "vgwm установлен в $VGWM_BIN" ;; | |
| 454 | + | ""|help|-h|--help) usage ;; | |
| 455 | + | *) usage; exit 1 ;; | |
| 456 | + | esac | |
| 457 | + | } | |
| 458 | + | main "$@" | |
Newer
Older