| 1 | #!/usr/bin/env bash |
| 2 | # |
| 3 | # add-ip.sh — Автоматическое добавление IP-адресов на Linux-сервер |
| 4 | # |
| 5 | # Поддерживает: Ubuntu (netplan), Debian/Astra (/etc/network/interfaces), |
| 6 | # CentOS/AlmaLinux (NetworkManager/nmcli), Arch Linux (systemd-networkd) |
| 7 | # |
| 8 | # Использование: |
| 9 | # sudo bash add-ip.sh |
| 10 | # |
| 11 | # Скрипт запросит нужные данные и применит конфигурацию автоматически. |
| 12 | |
| 13 | set -euo pipefail |
| 14 | |
| 15 | # ─── Цвета ─────────────────────────────────────────────────────────────────── |
| 16 | RED='\033[0;31m' |
| 17 | GREEN='\033[0;32m' |
| 18 | YELLOW='\033[1;33m' |
| 19 | CYAN='\033[0;36m' |
| 20 | NC='\033[0m' |
| 21 | |
| 22 | info() { echo -e "${CYAN}[INFO]${NC} $*"; } |
| 23 | ok() { echo -e "${GREEN}[OK]${NC} $*"; } |
| 24 | warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } |
| 25 | err() { echo -e "${RED}[ERROR]${NC} $*" >&2; } |
| 26 | die() { err "$@"; exit 1; } |
| 27 | |
| 28 | # ─── Проверка root ─────────────────────────────────────────────────────────── |
| 29 | if [[ $EUID -ne 0 ]]; then |
| 30 | die "Скрипт нужно запускать от root: sudo bash $0" |
| 31 | fi |
| 32 | |
| 33 | # ─── Определение дистрибутива ──────────────────────────────────────────────── |
| 34 | detect_distro() { |
| 35 | if [[ -f /etc/os-release ]]; then |
| 36 | . /etc/os-release |
| 37 | DISTRO_ID="${ID:-unknown}" |
| 38 | DISTRO_LIKE="${ID_LIKE:-}" |
| 39 | else |
| 40 | die "Не удалось определить дистрибутив (/etc/os-release не найден)" |
| 41 | fi |
| 42 | |
| 43 | case "$DISTRO_ID" in |
| 44 | ubuntu) DISTRO_FAMILY="ubuntu" ;; |
| 45 | debian) DISTRO_FAMILY="debian" ;; |
| 46 | astra) DISTRO_FAMILY="debian" ;; |
| 47 | centos|almalinux|rocky|rhel|fedora) |
| 48 | DISTRO_FAMILY="centos" ;; |
| 49 | arch|manjaro|endeavouros) |
| 50 | DISTRO_FAMILY="arch" ;; |
| 51 | *) |
| 52 | # Fallback: проверяем по ID_LIKE |
| 53 | if [[ "$DISTRO_LIKE" == *"debian"* ]]; then |
| 54 | DISTRO_FAMILY="debian" |
| 55 | elif [[ "$DISTRO_LIKE" == *"rhel"* || "$DISTRO_LIKE" == *"centos"* ]]; then |
| 56 | DISTRO_FAMILY="centos" |
| 57 | else |
| 58 | die "Неподдерживаемый дистрибутив: $DISTRO_ID ($DISTRO_LIKE)" |
| 59 | fi |
| 60 | ;; |
| 61 | esac |
| 62 | info "Дистрибутив: $DISTRO_ID (семейство: $DISTRO_FAMILY)" |
| 63 | } |
| 64 | |
| 65 | # ─── Определение интерфейса ───────────────────────────────────────────────── |
| 66 | detect_interface() { |
| 67 | # Берём первый нелупбэк интерфейс с IPv4 или IPv6 адресом (кроме виртуальных) |
| 68 | IFACE=$(ip -o link show up | awk -F': ' '{print $2}' | grep -v '^lo$' | grep -v '^docker' | grep -v '^br-' | grep -v '^veth' | head -n1) |
| 69 | if [[ -z "$IFACE" ]]; then |
| 70 | die "Не удалось определить сетевой интерфейс. Укажите вручную." |
| 71 | fi |
| 72 | info "Сетевой интерфейс: $IFACE" |
| 73 | } |
| 74 | |
| 75 | # ─── Сбор данных от пользователя ──────────────────────────────────────────── |
| 76 | collect_data() { |
| 77 | echo "" |
| 78 | echo -e "${CYAN}══════════════════════════════════════════════════════════════${NC}" |
| 79 | echo -e "${CYAN} Добавление IP-адресов на сервер${NC}" |
| 80 | echo -e "${CYAN}══════════════════════════════════════════════════════════════${NC}" |
| 81 | echo "" |
| 82 | |
| 83 | # Текущие адреса |
| 84 | info "Текущие IP-адреса на интерфейсе $IFACE:" |
| 85 | ip addr show "$IFACE" | grep -E 'inet ' | sed 's/^/ /' |
| 86 | echo "" |
| 87 | |
| 88 | # IPv4 |
| 89 | read -rp "Добавить IPv4-адрес? (например 192.0.2.70/24) [пусто = пропустить]: " IPV4_ADDR |
| 90 | if [[ -n "$IPV4_ADDR" ]]; then |
| 91 | # Извлекаем префикс если не указан |
| 92 | if [[ "$IPV4_ADDR" != *"/"* ]]; then |
| 93 | IPV4_ADDR="${IPV4_ADDR}/24" |
| 94 | warn "Маска не указана, используем /24" |
| 95 | fi |
| 96 | read -rp "IPv4 шлюз (первый адрес в сети, например 192.0.2.1): " IPV4_GW |
| 97 | [[ -z "$IPV4_GW" ]] && die "Шлюз обязателен для IPv4" |
| 98 | HAS_IPV4=true |
| 99 | else |
| 100 | HAS_IPV4=false |
| 101 | fi |
| 102 | |
| 103 | # IPv6 |
| 104 | read -rp "Добавить IPv6-адрес? (например 2001:db8:a:a::a/128) [пусто = пропустить]: " IPV6_ADDR |
| 105 | if [[ -n "$IPV6_ADDR" ]]; then |
| 106 | if [[ "$IPV6_ADDR" != *"/"* ]]; then |
| 107 | IPV6_ADDR="${IPV6_ADDR}/128" |
| 108 | warn "Маска не указана, используем /128" |
| 109 | fi |
| 110 | read -rp "IPv6 шлюз (например 2001:db8:a:a::1): " IPV6_GW |
| 111 | [[ -z "$IPV6_GW" ]] && die "Шлюз обязателен для IPv6" |
| 112 | HAS_IPV6=true |
| 113 | else |
| 114 | HAS_IPV6=false |
| 115 | fi |
| 116 | |
| 117 | if ! $HAS_IPV4 && ! $HAS_IPV6; then |
| 118 | die "Ни один адрес не указан. Выход." |
| 119 | fi |
| 120 | |
| 121 | # DNS |
| 122 | read -rp "DNS-серверы через запятую (по умолчанию 1.1.1.1,1.0.0.1): " DNS_INPUT |
| 123 | if [[ -z "$DNS_INPUT" ]]; then |
| 124 | DNS_V4=("1.1.1.1" "1.0.0.1") |
| 125 | DNS_V6=("2606:4700:4700::1111" "2606:4700:4700::1001") |
| 126 | else |
| 127 | IFS=',' read -ra DNS_V4 <<< "$DNS_INPUT" |
| 128 | DNS_V6=("${DNS_V4[@]}") |
| 129 | fi |
| 130 | |
| 131 | # Подтверждение |
| 132 | echo "" |
| 133 | echo -e "${YELLOW}══════════════════════════════════════════════════════════════${NC}" |
| 134 | echo -e "${YELLOW} Итого:${NC}" |
| 135 | echo -e "${YELLOW} Интерфейс: ${NC}$IFACE" |
| 136 | $HAS_IPV4 && echo -e "${YELLOW} IPv4: ${NC}$IPV4_ADDR шлюз: $IPV4_GW" |
| 137 | $HAS_IPV6 && echo -e "${YELLOW} IPv6: ${NC}$IPV6_ADDR шлюз: $IPV6_GW" |
| 138 | echo -e "${YELLOW} DNS v4: ${NC}${DNS_V4[*]}" |
| 139 | echo -e "${YELLOW} DNS v6: ${NC}${DNS_V6[*]}" |
| 140 | echo -e "${YELLOW}══════════════════════════════════════════════════════════════${NC}" |
| 141 | echo "" |
| 142 | read -rp "Всё верно? Применить? (y/N): " CONFIRM |
| 143 | [[ "$CONFIRM" =~ ^[YyДд]$ ]] || die "Отмена пользователем." |
| 144 | } |
| 145 | |
| 146 | # ─── Общие шаги: sysctl + cloud-init ──────────────────────────────────────── |
| 147 | setup_sysctl_nonlocal_bind() { |
| 148 | info "Настройка ip_nonlocal_bind..." |
| 149 | local SYSCTL_FILE="/etc/sysctl.d/99-nonlocal-bind.conf" |
| 150 | cat > "$SYSCTL_FILE" <<'EOF' |
| 151 | net.ipv4.ip_nonlocal_bind = 1 |
| 152 | net.ipv6.ip_nonlocal_bind = 1 |
| 153 | EOF |
| 154 | sysctl --system >/dev/null 2>&1 |
| 155 | ok "ip_nonlocal_bind включён" |
| 156 | } |
| 157 | |
| 158 | disable_cloud_init_network() { |
| 159 | local CLOUD_CFG="/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg" |
| 160 | if [[ ! -f "$CLOUD_CFG" ]]; then |
| 161 | info "Отключение cloud-init network config..." |
| 162 | mkdir -p /etc/cloud/cloud.cfg.d/ |
| 163 | echo 'network: {config: disabled}' > "$CLOUD_CFG" |
| 164 | ok "cloud-init network config отключён" |
| 165 | else |
| 166 | ok "cloud-init network config уже отключён" |
| 167 | fi |
| 168 | } |
| 169 | |
| 170 | # ─── Ubuntu (netplan) ─────────────────────────────────────────────────────── |
| 171 | configure_ubuntu() { |
| 172 | info "Конфигурация через netplan..." |
| 173 | |
| 174 | # Бэкап существующих файлов |
| 175 | for f in /etc/netplan/*.yaml; do |
| 176 | if [[ -f "$f" && "$f" != *"99-ipv"* ]]; then |
| 177 | local backup="${f}-backup-$(date +%Y%m%d%H%M%S)" |
| 178 | mv "$f" "$backup" |
| 179 | info "Создан бэкап: $backup" |
| 180 | fi |
| 181 | done |
| 182 | |
| 183 | # --- IPv4 --- |
| 184 | if $HAS_IPV4; then |
| 185 | cat > /etc/netplan/99-ipv4.yaml <<YAML |
| 186 | network: |
| 187 | version: 2 |
| 188 | renderer: networkd |
| 189 | ethernets: |
| 190 | ${IFACE}: |
| 191 | dhcp4: false |
| 192 | addresses: |
| 193 | - "${IPV4_ADDR}" |
| 194 | routes: |
| 195 | - to: "0.0.0.0/0" |
| 196 | via: "${IPV4_GW}" |
| 197 | nameservers: |
| 198 | addresses: |
| 199 | $(printf ' - "%s"\n' "${DNS_V4[@]}") |
| 200 | YAML |
| 201 | chmod 600 /etc/netplan/99-ipv4.yaml |
| 202 | ok "Создан /etc/netplan/99-ipv4.yaml" |
| 203 | fi |
| 204 | |
| 205 | # --- IPv6 --- |
| 206 | if $HAS_IPV6; then |
| 207 | cat > /etc/netplan/99-ipv6.yaml <<YAML |
| 208 | network: |
| 209 | version: 2 |
| 210 | renderer: networkd |
| 211 | ethernets: |
| 212 | ${IFACE}: |
| 213 | accept-ra: false |
| 214 | dhcp6: false |
| 215 | addresses: |
| 216 | - "${IPV6_ADDR}" |
| 217 | routes: |
| 218 | - to: "::/0" |
| 219 | via: "${IPV6_GW}" |
| 220 | on-link: true |
| 221 | nameservers: |
| 222 | addresses: |
| 223 | $(printf ' - "%s"\n' "${DNS_V6[@]}") |
| 224 | YAML |
| 225 | chmod 600 /etc/netplan/99-ipv6.yaml |
| 226 | ok "Создан /etc/netplan/99-ipv6.yaml" |
| 227 | fi |
| 228 | |
| 229 | # Отключение dhcpcd6 |
| 230 | if systemctl list-unit-files | grep -q 'dhcpcd6.service'; then |
| 231 | systemctl disable dhcpcd6.service 2>/dev/null || true |
| 232 | fi |
| 233 | if systemctl list-unit-files | grep -q 'dhclient6.service'; then |
| 234 | systemctl disable dhclient6.service 2>/dev/null || true |
| 235 | fi |
| 236 | |
| 237 | systemctl daemon-reload 2>/dev/null || true |
| 238 | |
| 239 | setup_sysctl_nonlocal_bind |
| 240 | disable_cloud_init_network |
| 241 | |
| 242 | info "Применение netplan..." |
| 243 | netplan --debug apply 2>&1 || warn "netplan apply вернул ошибку — попробуйте перезагрузку" |
| 244 | ok "Netplan конфигурация применена" |
| 245 | } |
| 246 | |
| 247 | # ─── Debian/Astra (/etc/network/interfaces) ────────────────────────────────── |
| 248 | configure_debian() { |
| 249 | info "Конфигурация через /etc/network/interfaces..." |
| 250 | |
| 251 | local IFACE_FILE="/etc/network/interfaces" |
| 252 | local BACKUP="${IFACE_FILE}-backup-$(date +%Y%m%d%H%M%S)" |
| 253 | [[ -f "$IFACE_FILE" ]] && cp "$IFACE_FILE" "$BACKUP" && info "Бэкап: $BACKUP" |
| 254 | |
| 255 | # Генерируем новый конфиг |
| 256 | { |
| 257 | echo "auto lo" |
| 258 | echo "iface lo inet loopback" |
| 259 | echo "" |
| 260 | |
| 261 | if $HAS_IPV4; then |
| 262 | echo "auto ${IFACE}" |
| 263 | echo "allow-hotplug ${IFACE}" |
| 264 | echo "iface ${IFACE} inet static" |
| 265 | echo -e "\taddress ${IPV4_ADDR}" |
| 266 | echo -e "\tgateway ${IPV4_GW}" |
| 267 | echo -e "\tdns-nameservers ${DNS_V4[*]}" |
| 268 | echo "" |
| 269 | fi |
| 270 | |
| 271 | if $HAS_IPV6; then |
| 272 | echo "auto ${IFACE}" |
| 273 | echo "allow-hotplug ${IFACE}" |
| 274 | echo "iface ${IFACE} inet6 static" |
| 275 | echo -e "\taddress ${IPV6_ADDR}" |
| 276 | echo -e "\tgateway ${IPV6_GW}" |
| 277 | echo -e "\tdns-nameservers ${DNS_V6[*]}" |
| 278 | fi |
| 279 | } > "$IFACE_FILE" |
| 280 | |
| 281 | ok "Записан $IFACE_FILE" |
| 282 | |
| 283 | # Бэкап cloud-init файла |
| 284 | local INTERFACES_D="/etc/network/interfaces.d" |
| 285 | if [[ -d "$INTERFACES_D" ]]; then |
| 286 | for f in "$INTERFACES_D"/*; do |
| 287 | [[ -f "$f" ]] && mv "$f" "/root/$(basename "$f")-backup-$(date +%Y%m%d%H%M%S)" 2>/dev/null || true |
| 288 | done |
| 289 | fi |
| 290 | |
| 291 | # Отключение dhcpcd6 |
| 292 | if systemctl list-unit-files | grep -q 'dhcpcd6.service'; then |
| 293 | systemctl disable dhcpcd6.service 2>/dev/null || true |
| 294 | fi |
| 295 | if systemctl list-unit-files | grep -q 'dhclient6.service'; then |
| 296 | systemctl disable dhclient6.service 2>/dev/null || true |
| 297 | fi |
| 298 | |
| 299 | systemctl daemon-reload 2>/dev/null || true |
| 300 | |
| 301 | setup_sysctl_nonlocal_bind |
| 302 | disable_cloud_init_network |
| 303 | |
| 304 | info "Перезапуск networking..." |
| 305 | systemctl restart networking.service 2>&1 || warn "networking restart вернул ошибку — попробуйте перезагрузку" |
| 306 | ok "Конфигурация применена" |
| 307 | } |
| 308 | |
| 309 | # ─── CentOS/AlmaLinux (NetworkManager / nmcli) ────────────────────────────── |
| 310 | configure_centos() { |
| 311 | info "Конфигурация через NetworkManager (nmcli)..." |
| 312 | |
| 313 | # Определяем имя соединения |
| 314 | local CONN_NAME |
| 315 | CONN_NAME=$(nmcli -t -f NAME,DEVICE connection show | grep ":${IFACE}$" | head -n1 | cut -d: -f1) |
| 316 | if [[ -z "$CONN_NAME" ]]; then |
| 317 | die "Не удалось найти соединение для интерфейса $IFACE. Проверьте 'nmcli connection show'" |
| 318 | fi |
| 319 | info "Имя соединения: $CONN_NAME" |
| 320 | |
| 321 | # Бэкап текущих настроек |
| 322 | local CONN_DIR="/etc/NetworkManager/system-connections" |
| 323 | local CONN_FILE="" |
| 324 | for f in "$CONN_DIR"/*; do |
| 325 | if [[ -f "$f" ]] && grep -q "interface-name=${IFACE}" "$f" 2>/dev/null; then |
| 326 | CONN_FILE="$f" |
| 327 | break |
| 328 | fi |
| 329 | done |
| 330 | if [[ -n "$CONN_FILE" ]]; then |
| 331 | local BACKUP="${CONN_FILE}-backup-$(date +%Y%m%d%H%M%S)" |
| 332 | cp "$CONN_FILE" "$BACKUP" |
| 333 | info "Бэкап: $BACKUP" |
| 334 | fi |
| 335 | |
| 336 | # IPv4 |
| 337 | if $HAS_IPV4; then |
| 338 | nmcli connection modify "$CONN_NAME" ipv4.addresses "$IPV4_ADDR" |
| 339 | nmcli connection modify "$CONN_NAME" ipv4.gateway "$IPV4_GW" |
| 340 | nmcli connection modify "$CONN_NAME" ipv4.method manual |
| 341 | nmcli connection modify "$CONN_NAME" ipv4.dns "${DNS_V4[0]}" |
| 342 | for dns in "${DNS_V4[@]:1}"; do |
| 343 | nmcli connection modify "$CONN_NAME" +ipv4.dns "$dns" |
| 344 | done |
| 345 | ok "IPv4 настроены: $IPV4_ADDR, шлюз $IPV4_GW" |
| 346 | fi |
| 347 | |
| 348 | # IPv6 |
| 349 | if $HAS_IPV6; then |
| 350 | nmcli connection modify "$CONN_NAME" ipv6.addresses "$IPV6_ADDR" |
| 351 | nmcli connection modify "$CONN_NAME" ipv6.gateway "$IPV6_GW" |
| 352 | nmcli connection modify "$CONN_NAME" ipv6.method manual |
| 353 | nmcli connection modify "$CONN_NAME" ipv6.dns "${DNS_V6[0]}" |
| 354 | for dns in "${DNS_V6[@]:1}"; do |
| 355 | nmcli connection modify "$CONN_NAME" +ipv6.dns "$dns" |
| 356 | done |
| 357 | ok "IPv6 настроены: $IPV6_ADDR, шлюз $IPV6_GW" |
| 358 | fi |
| 359 | |
| 360 | # Бэкап старых ifcfg файлов |
| 361 | local NETSCRIPTS="/etc/sysconfig/network-scripts" |
| 362 | if [[ -d "$NETSCRIPTS" ]]; then |
| 363 | for f in "$NETSCRIPTS"/ifcfg-*; do |
| 364 | [[ -f "$f" && "$f" != *"backup"* ]] && mv "$f" "/root/$(basename "$f")-backup-$(date +%Y%m%d%H%M%S)" 2>/dev/null || true |
| 365 | done |
| 366 | fi |
| 367 | |
| 368 | setup_sysctl_nonlocal_bind |
| 369 | disable_cloud_init_network |
| 370 | |
| 371 | info "Перезапуск NetworkManager..." |
| 372 | systemctl restart NetworkManager.service 2>&1 || warn "NetworkManager restart вернул ошибку — попробуйте перезагрузку" |
| 373 | ok "Конфигурация применена" |
| 374 | } |
| 375 | |
| 376 | # ─── Arch Linux (systemd-networkd) ────────────────────────────────────────── |
| 377 | configure_arch() { |
| 378 | info "Конфигурация через systemd-networkd..." |
| 379 | |
| 380 | local NETDIR="/etc/systemd/network" |
| 381 | mkdir -p "$NETDIR" |
| 382 | |
| 383 | # Бэкап старых конфигов |
| 384 | for f in "$NETDIR"/*.network; do |
| 385 | [[ -f "$f" ]] && mv "$f" "${f}_old" 2>/dev/null || true |
| 386 | done |
| 387 | |
| 388 | # Включаем нужные сервисы |
| 389 | systemctl enable --now systemd-networkd.service 2>/dev/null || true |
| 390 | systemctl enable --now systemd-resolved.service 2>/dev/null || true |
| 391 | |
| 392 | # IPv4 |
| 393 | if $HAS_IPV4; then |
| 394 | cat > "$NETDIR/10-static-ipv4.network" <<EOF |
| 395 | [Match] |
| 396 | Name=${IFACE} |
| 397 | |
| 398 | [Network] |
| 399 | Address=${IPV4_ADDR} |
| 400 | Gateway=${IPV4_GW} |
| 401 | $(printf 'DNS=%s\n' "${DNS_V4[@]}") |
| 402 | DHCP=no |
| 403 | EOF |
| 404 | ok "Создан $NETDIR/10-static-ipv4.network" |
| 405 | fi |
| 406 | |
| 407 | # IPv6 |
| 408 | if $HAS_IPV6; then |
| 409 | cat > "$NETDIR/20-static-ipv6.network" <<EOF |
| 410 | [Match] |
| 411 | Name=${IFACE} |
| 412 | |
| 413 | [Network] |
| 414 | Address=${IPV6_ADDR} |
| 415 | $(printf 'DNS=%s\n' "${DNS_V6[@]}") |
| 416 | DHCP=no |
| 417 | IPv6AcceptRA=no |
| 418 | |
| 419 | [Route] |
| 420 | Destination=::/0 |
| 421 | Gateway=${IPV6_GW} |
| 422 | GatewayOnLink=yes |
| 423 | EOF |
| 424 | ok "Создан $NETDIR/20-static-ipv6.network" |
| 425 | fi |
| 426 | |
| 427 | setup_sysctl_nonlocal_bind |
| 428 | |
| 429 | info "Перезапуск systemd-networkd..." |
| 430 | systemctl restart systemd-networkd 2>&1 || warn "systemd-networkd restart вернул ошибку — попробуйте перезагрузку" |
| 431 | ok "Конфигурация применена" |
| 432 | } |
| 433 | |
| 434 | # ─── Проверка результата ──────────────────────────────────────────────────── |
| 435 | verify() { |
| 436 | echo "" |
| 437 | info "Проверяем IP-адреса на интерфейсе $IFACE:" |
| 438 | echo "" |
| 439 | ip addr show "$IFACE" | grep -E 'inet ' | sed 's/^/ /' |
| 440 | echo "" |
| 441 | ok "Готово! Рекомендуется перезагрузить сервер для проверки: sudo reboot" |
| 442 | } |
| 443 | |
| 444 | # ─── main ──────────────────────────────────────────────────────────────────── |
| 445 | main() { |
| 446 | detect_distro |
| 447 | detect_interface |
| 448 | collect_data |
| 449 | |
| 450 | case "$DISTRO_FAMILY" in |
| 451 | ubuntu) configure_ubuntu ;; |
| 452 | debian) configure_debian ;; |
| 453 | centos) configure_centos ;; |
| 454 | arch) configure_arch ;; |
| 455 | esac |
| 456 | |
| 457 | verify |
| 458 | } |
| 459 | |
| 460 | main "$@" |
| 461 |
KarelWintersky / add_ip.sh
Last active 15 hours ago
Revision 4f9315dc2205a90175df5337e414efad6a03d604