Сервер из старого компьютера: Wi-Fi, который не переживает перезагрузку, доступ из интернета и защита

Сервер из старого компьютера: Wi-Fi, который не переживает перезагрузку, доступ из интернета и защита

Для пет-проекта мне нужен был сервер, который круглосуточно будет работать в кафе (система автоматизации для бизнеса моего кореша): на нём работают ИИ-агент, база данных и Telegram-бот. Покупать ничего не хотелось. В итоге нашёлся старый системный блок с двухъядерным Pentium E5400, 4 ГБ памяти и Ubuntu 18.04. Кабеля до роутера там нет, только Wi-Fi через USB-адаптер.

В статье расскажу, как я превратил его в сервер, к которому можно подключиться из любой точки мира, и что пришлось сделать, чтобы он сам переживал перезагрузки и обрывы связи. Спойлер - самой сложной частью оказался Wi-Fi-адаптер, который после перезагрузки отказывается подключаться к сети.

Что за железо

Первым делом выясняем, с чем имеем дело:

lsb_release -a          # версия Ubuntu
lscpu                   # процессор, ядра, поддержка виртуализации (строка Virtualization)
free -h                 # память
df -h /                 # диск
lsusb                   # что подключено по USB (наш Wi-Fi-адаптер)

Pentium E5400 — 64-битный процессор из 2009 года. Для Docker с несколькими контейнерами его хватает, но любая сборка образов идёт неторопливо.

Ubuntu 18.04 давно без поддержки, поэтому я обновил систему до 20.04 через do-release-upgrade. Дальше 20.04 этот компьютер обновлять не стал: свежие версии на таком железе — лишний риск, а 20.04 ещё можно поддерживать через бесплатный Ubuntu Pro (до пяти машин). Учтем, что последний Docker для 20.04 — версии 28.1.

Wi-Fi: выбор адаптера

У меня было несколько адаптеров. Первый адаптер, Tenda, система вообще не увидела: драйвера в ядре нет, а собирать его из исходников под каждое обновление ядра не хотелось. Второй, TP-Link TL-WN821N v5 на чипе RTL8192EU, заработал сразу на встроенном в ядро драйвере rtl8xxxu. Интерфейс появился с именем вида wlx<MAC-адрес>.

Совет: перед покупкой адаптера для Linux-сервера проверьте, есть ли драйвер для его чипа в ядре. Название чипа важнее бренда, и у разных ревизий одной модели чипы бывают разные.

Сеть в Ubuntu Server настраивается через netplan. У меня файл назывался /etc/netplan/50-cloud-init.yaml, то есть его создал cloud-init:

network:
  version: 2
  renderer: networkd
  wifis:
    wlx0123456789ab:
      dhcp4: true
      dhcp6: false
      optional: true
      nameservers:
        addresses: [8.8.8.8, 8.8.4.4]
      access-points:
        "Название сети":
          password: "пароль"

Два важных момента:

  • optional: true — без этого загрузка ждёт сеть несколько минут, если Wi-Fi не поднялся.
  • cloud-init может перезаписать этот файл. Об этом предупреждает комментарий в его начале. Чтобы не потерять настройки, отключаем управление сетью из cloud-init:
echo 'network: {config: disabled}' | sudo tee /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg

sudo netplan apply на Ubuntu 18.04 и 20.04 с Wi-Fi выдаёт предупреждение про изменённый на диске юнит. Оно безобидное.

Главная проблема: после перезагрузки Wi-Fi не подключается

После первой же перезагрузки сервер пропал из сети. На экране ping писал Temporary failure in name resolution, ip addr показывал интерфейс без адреса, а в журнале wpa_supplicant было:

CTRL-EVENT-SSID-TEMP-DISABLED ... reason=CONN_FAILED auth_failures=6

Похоже на неверный пароль, но пароль был правильный. А помогало вот что: вынуть адаптер, вставить обратно и выполнить netplan apply.

Объяснение такое. При перезагрузке без отключения питания USB-порт не обесточивается, и чип адаптера не сбрасывается. Драйвер rtl8xxxu загружается заново, но застаёт чип в состоянии, которое оставила прошлая сессия, и рукопожатие с точкой доступа не проходит. Отсюда «ошибки авторизации» при правильном пароле. Когда адаптер вынимают, чип теряет питание и стартует с чистого листа. Это известная беда с несколькими чипами Realtek на этом драйвере.

Ходить в кафе передёргивать адаптер после каждой перезагрузки — не вариант. Нужно передёрнуть его программно.

Программное «вынуть и вставить»

У каждого USB-устройства в Linux есть файл authorized в sysfs. Если записать в него 0, ядро отключит устройство, как будто его вынули. Если записать 1, подключит обратно, с повторной инициализацией и загрузкой драйвера. Питание порт при этом не снимает, но для нашего адаптера такого сброса хватает.

Путь к устройству удобно найти от сетевого интерфейса: /sys/class/net/<интерфейс>/device указывает на USB-интерфейс адаптера, а уровнем выше лежит само USB-устройство.

Скрипт /usr/local/sbin/wifi-reset.sh:

#!/bin/sh
set -eu
. /etc/default/wifi-watchdog     # WIFI_IF=wlx0123456789ab

dev_path=$(readlink -f "/sys/class/net/$WIFI_IF/device/..") || { echo "нет интерфейса $WIFI_IF"; exit 1; }
echo 0 > "$dev_path/authorized"
sleep 3
echo 1 > "$dev_path/authorized"

# Ждём, пока интерфейс появится снова
i=0
while [ ! -e "/sys/class/net/$WIFI_IF" ] && [ $i -lt 30 ]; do sleep 1; i=$((i + 1)); done
[ -e "/sys/class/net/$WIFI_IF" ] || { echo "интерфейс не вернулся"; exit 1; }

iw dev "$WIFI_IF" set power_save off 2>/dev/null || true
netplan apply 2>/dev/null

Путь вычисляется в момент запуска, поэтому скрипт не сломается, если адаптер воткнуть в другой порт: имя wlx… построено из MAC-адреса и не меняется. Заодно выключаем энергосбережение Wi-Fi. На серверах оно чаще мешает, чем помогает: адаптер «засыпает», и входящие соединения начинают тормозить.

Ручной запуск при живой сети: адаптер пропадает и через 12 секунд снова на связи.

Сброс при загрузке и сторож

Сброс нужен в двух ситуациях: сразу после загрузки и когда связь пропала посреди работы.

При загрузке — одноразовая служба wifi-reset-boot.service:

[Unit]
Description=Сброс USB-адаптера Wi-Fi при загрузке
After=systemd-networkd.service
Wants=systemd-networkd.service

[Service]
Type=oneshot
ExecStartPre=/bin/sleep 10
ExecStart=/usr/local/sbin/wifi-reset.sh

[Install]
WantedBy=multi-user.target

Пауза в 10 секунд даёт системе спокойно подняться. Сбрасывать адаптер, пока драйвер ещё загружается, — плохая идея.

Сторож (watchdog) — каждые две минуты проверяет, есть ли у интерфейса адрес и отвечает ли роутер:

#!/bin/sh
set -eu
. /etc/default/wifi-watchdog     # WIFI_IF, WIFI_GATEWAY

gw=$(ip -4 route show default dev "$WIFI_IF" 2>/dev/null | awk '{print $3; exit}')
gw=${gw:-$WIFI_GATEWAY}
if ip -4 addr show "$WIFI_IF" | grep -q 'inet ' && ping -c 3 -W 3 -I "$WIFI_IF" "$gw" >/dev/null 2>&1; then
  exit 0
fi
echo "нет связи с $gw — сбрасываю адаптер"
exec /usr/local/sbin/wifi-reset.sh

Пингуем именно роутер, а не 8.8.8.8. Если интернет пропал у провайдера, сбрасывать адаптер бесполезно, а пинг роутера проверяет ровно то, за что отвечает адаптер. Адрес роутера берём из маршрута по умолчанию. Если маршрута нет, потому что сеть уже упала, берём адрес из настроек.

Запуск — таймер systemd вместо cron:

# wifi-watchdog.timer
[Timer]
OnBootSec=3min
OnUnitActiveSec=2min

[Install]
WantedBy=timers.target

OnBootSec=3min — первая проверка через три минуты после загрузки, когда сброс при загрузке уже отработал. Логи пишутся в journald, и их видно через journalctl -u wifi-watchdog. Таймер, в отличие от cron, не запустит новую проверку, пока не закончилась предыдущая.

Честно: ручной сброс я проверил, а полноценную перезагрузку с этой схемой ещё нет — сделаю, когда буду рядом с сервером. Если что-то пойдёт не так, лучше стоять у компьютера, а не в другом конце города.

Доступ из интернета

У кафе статический «белый» IP-адрес, то есть роутер доступен из интернета напрямую. Осталось три шага.

1. Проброс порта на роутере. Внешний порт 22022 пробрасывается на порт 22 сервера. Внешний порт нестандартный: защиты это почти не добавляет, но в логах сразу становится в разы меньше шума от ботов, которые перебирают пароли на 22-м.

Проброс указывает на локальный адрес сервера, поэтому этот адрес не должен меняться. Закрепите его на роутере за MAC-адресом адаптера (DHCP reservation) или пропишите статический адрес в netplan.

2. Имя вместо IP — DuckDNS. Можно подключаться и по IP, но имя удобнее запомнить. А если провайдер когда-нибудь сменит адрес, имя продолжит работать. DuckDNS бесплатно даёт поддомен вида myserver.duckdns.org и обновляет его простым HTTP-запросом:

#!/bin/sh
set -eu
. /etc/default/duckdns     # DUCKDNS_DOMAIN, DUCKDNS_TOKEN; файл доступен только root (chmod 600)
# Токен — через stdin (curl -K -), чтобы он не попал в список процессов
answer=$(printf 'url = "https://www.duckdns.org/update?domains=%s&token=%s&ip="\n' \
  "$DUCKDNS_DOMAIN" "$DUCKDNS_TOKEN" | curl -fsS -m 30 -K -)
[ "$answer" = "OK" ] || { echo "DuckDNS ответил: $answer"; exit 1; }

Пустой ip= означает «возьми адрес, с которого пришёл запрос». Скрипт запускается таймером раз в 5 минут.

Почему токен передаётся не в аргументах curl: аргументы любого процесса видны всем пользователям системы через ps. Конструкция curl -K - читает параметры из stdin, и токен в списке процессов не появляется.

На сайте DuckDNS есть готовая инструкция с cron и echo url=... | curl -K -. Мой вариант делает то же, только на таймере systemd, с проверкой ответа и с токеном в защищённом файле, а не в crontab.

3. Ключи SSH. На ноутбуке — отдельный ключ для этого сервера, на сервере он добавлен в ~/.ssh/authorized_keys.

Ловушка: изнутри сети внешний адрес не работает

Проверяю доступ, сидя в том же кафе: ssh -p 22022 myserver.duckdns.org и получаю Connection refused. Снаружи, через мобильный интернет, всё работает.

Это hairpin NAT, или NAT loopback. Запрос из локальной сети на внешний адрес роутера должен «развернуться» на роутере обратно внутрь. Многие домашние роутеры этого не умеют. Чинить роутер не обязательно: достаточно двух записей в ~/.ssh/config:

Host myserver
    HostName 192.168.1.10
    User admin
    IdentityFile ~/.ssh/id_myserver

Host myserver-ext
    HostName myserver.duckdns.org
    Port 22022
    User admin
    IdentityFile ~/.ssh/id_myserver

В кафе — ssh myserver, из любого другого места — ssh myserver-ext.

При первом подключении по новому имени SSH спросит, доверять ли ключу хоста. Не отвечайте yes не глядя: сверьте отпечаток с тем, что показывает сам сервер, когда вы точно подключены к нему по локальной сети:

ssh myserver 'ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub'

Если отпечатки совпадают, на том конце действительно ваш сервер.

fail2ban: что делать с теми, кто подбирает пароль

Как только порт SSH открыт в интернет, в логах появляются тысячи попыток входа. fail2ban следит за логом и банит адреса через файрвол. Настройки — файл в /etc/fail2ban/jail.d/, например sshd.local:

[sshd]
enabled = true
port = 22
maxretry = 5
findtime = 10m
bantime = 1h
bantime.increment = true
bantime.maxtime = 1w
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
  • 5 неудачных попыток за 10 минут — бан на час.
  • bantime.increment — каждый следующий бан того же адреса длиннее, до недели. Упорные боты быстро уходят надолго.
  • port = 22 — порт на самом сервере. Внешний 22022 существует только на роутере.
  • ignoreip — локальная сеть не банится никогда. Если вы ошибётесь с паролем, сидя в кафе, вас не отрежет от собственного сервера.

Важная проверка: убедитесь, что сервер видит настоящие внешние адреса подключающихся, а не адрес роутера. Некоторые роутеры подменяют адрес источника при пробросе портов. Тогда fail2ban забанит сам роутер, а заодно и всех. Посмотрите sudo journalctl -u ssh после входа снаружи: в строках Accepted publickey for ... from <адрес> должен быть ваш внешний IP, а не 192.168.x.x.

Ещё правильнее отключить вход по паролю совсем (PasswordAuthentication no). Я пока оставил пароль как запасной путь на случай потери ключа. С таким выбором fail2ban обязателен, а пароль должен быть длинным и случайным.

Всё остальное — только через SSH-туннель

На сервере есть веб-интерфейс n8n и удалённый рабочий стол, XFCE + xrdp, на случай если понадобится что-то сделать мышкой. Пробрасывать их порты в интернет я не стал. Они слушают только localhost сервера, а я хожу к ним через SSH:

Host myserver-n8n
    HostName myserver.duckdns.org
    Port 22022
    User admin
    IdentityFile ~/.ssh/id_myserver
    LocalForward 15678 localhost:5678

Host myserver-rdp
    HostName myserver.duckdns.org
    Port 22022
    User admin
    IdentityFile ~/.ssh/id_myserver
    LocalForward 13389 localhost:3389

ssh -N myserver-n8n держит туннель, а в браузере открывается http://localhost:15678. -N означает «не открывать командную строку, только туннель». Порт на ноутбуке 15678, а не 5678, потому что 5678 занят моим локальным n8n для разработки.

В Docker Compose порт n8n опубликован только на loopback:

ports:
  - "127.0.0.1:5678:5678"

Без 127.0.0.1: Docker опубликует порт на всех интерфейсах. Причём он обходит обычные правила ufw, потому что пишет свои правила в iptables. Это частая причина «случайно открытых» баз данных и админок.

Что получилось: снаружи открыт ровно один порт, SSH. Он защищён ключами и fail2ban, а всё остальное спрятано за ним.

Итог

Задача Решение
Wi-Fi без кабеля Адаптер с драйвером в ядре (RTL8192EU, rtl8xxxu), netplan, optional: true
Адаптер не подключается после перезагрузки Программный «вынуть-вставить» через authorized в sysfs при загрузке
Обрывы связи Сторож на таймере systemd: пинг роутера раз в 2 минуты, сброс адаптера
Доступ из интернета Белый IP, нестандартный внешний порт, DuckDNS
Изнутри не работает внешний адрес Два хоста в ~/.ssh/config: локальный и внешний
Подбор паролей fail2ban с нарастающим баном, локальная сеть в исключениях
Веб-интерфейсы Только 127.0.0.1 и SSH-туннель

Старый компьютер, недорогой USB-адаптер и бесплатный DuckDNS — и у проекта есть свой сервер. Из говна и палок - зато без абонентской платы )

Подписывайтесь на мой канал в Tg про разработку – Sleepless Tech