Arch Linux: “містичні” таймаути з DNS та “в пошуках Ethernet-істини”
0 (0)

19 Січня 2026

Вже пару місяців, як на робочому ноуті Lenovo ThinkPad T14 Gen 5 з Arch Linux виникла проблема з відкриттям нових сайтів – перші 10-15 секунд сайт завантажується “шматочками”, наприклад:

Але потім “розчехляється”, і все починає працювати чудово:

Нарешті, як почав налаштовувати нормальну домашню мережу з VPN (див. FreeBSD: Home NAS, part 3 – WireGuard VPN, Linux peer та routing), а потім для неї – DNS (див. FreeBSD: Home NAS, part 4 – локальний DNS з Unbound), то дійшли руки розібратись і з цієї проблемою.

І проблема виявилась дуже цікавою. Причину шукав довго, і перепровірив купу різних налаштувань – від IPv6 і DNS до драйверу мережової карти.

Головне, що проблема не те щоб була критичною – в цілому інет працював, а тому я іноді починав шукати причину, потім закидував, потім знов повертався.

The issue: “communications error to 192.168.0.1#53: timed out”

Що цікаво, що проблема спостерігалась тільки на Ethernet-підключені – на WiFi все працювало чудово.

А на Ethernet репродьюсилось на різних кабелях і через різні роутери.

Значить – що? Значить – або Сєня щось наковиряв руками у своєму Linux, або десь колись прилетів “кривий” апдейт чи до ядра, чи до драйвера, чи до якось бібліотеки.

Вже не пам’ятаю чому, але спершу грішив на DNS, бо ми ж знаємо, що:

І такі да – під час спроб зарепродьюсити це вдалось саме з DNS, під час тестів з dig – тому довго копав в цю сторону.

Виглядала проблема так: робимо dig, 10-15 запитів проходять нормально, а потім прилітає “communications error to 192.168.0.1#53: timed out“:

$ time dig google.com +short @192.168.0.1
;; communications error to 192.168.0.1#53: timed out
...

real    0m5.018s
user    0m0.004s
sys     0m0.008s

Ну і це виглядало, як дійсно причина того, що сайти тупили з загрузкою контенту: якщо DNS періодично відвалюється, а сайти мають купу додаткових скриптів і картинок, які підвантажуються з інших ресурсів – то поки всі хости разрезолвляться, поки отримаємо всі адреси, поки почнеться завантаження – як раз маємо цю затримку в кількадесят секунд.

Логічно? Так.

Тому і всі подальше тести я робив вже в циклі з dig:

$ for i in {1..50}; do { time dig +nocookie +noedns +tries=1 +time=2 google.com >/dev/null; } 2>&1; done
...
real    0m0.016s
...
real    0m2.015s
...
real    0m0.013s
...
real    0m1.392s

І такий результат був постійно – пачка запитів проходить нормально – “real 0m0.016s“, а потім на якомусь одному – таймаут і “real 0m2.015s” (бо +time=2 – чекати 2 секунди, а не дефолтні 5).

Ця ж проблема була видна з tcpdump: в 09:57:47 запит відправлений, але відповіді не отримано, через 2 секунди, в 09:57:49 – новий запит, і на нього вже відповідь прийшла:

...
09:57:47.717951 IP setevoy-work.40923 > _gateway.domain: 13058+ [1au] A? google.com. (51)
09:57:49.729589 IP setevoy-work.45441 > _gateway.domain: 63641+ [1au] A? google.com. (51)
09:57:49.730249 IP _gateway.domain > setevoy-work.45441: 63641 6/4/4 A 142.250.109.101, A 142.250.109.100, A 142.250.109.139, A 142.250.109.138, A 142.250.109.102, A 142.250.109.113 (260)
...

Аналогічно видна проблема з strace:

$ strace -r -e trace=network dig google.com
...
     0.002788 socket(AF_INET, SOCK_DGRAM, IPPROTO_IP) = 15
...
     ;; communications error to 192.168.0.1#53: timed out
     5.005754 socket(AF_INET, SOCK_DGRAM, IPPROTO_IP) = 16
...

Тут в 0.002788 відкритий сокет для відправки запиту, а через 5 секунд (5.005754) – бо зараз dig запускався без +time=2 – відкривається новий сокет для нового запиту, бо на попередній відповіді не було.

В пошуках Немо проблеми

Тут опишу що взагалі перевіряв – квест вийшов той ще.

Хоча записав не все, робив більше, але основне зберіг – вже давно є звичка закидувати в чорнетку поста на RTFM під час дебагу проблем.

Перевірка DNS в Linux

Перше – що з DNS в системі?

В /etc/resolv.conf заданий роутер:

# Generated by NetworkManager
nameserver 192.168.0.1

Міняємо на 1.1.1.1 чи на 8.8.8.8 – проблема залишається.

Окей… Може, в системі ще якийсь активний резолвер, і починається “DNS-гонка в ядрі” – запит “блукає” між ними?

Перевіряємо systemd-resolved – ні, не запущений:

$ systemctl status systemd-resolved
○ systemd-resolved.service - Network Name Resolution
     Loaded: loaded (/usr/lib/systemd/system/systemd-resolved.service; disabled; preset: enabled)
     Active: inactive (dead)
...

Може, dnsmasq?

Теж виключений:

$ systemctl status dnsmasq
○ dnsmasq.service - dnsmasq - A lightweight DHCP and caching DNS server
     Loaded: loaded (/usr/lib/systemd/system/dnsmasq.service; disabled; preset: disabled)
     Active: inactive (dead)
...

Значить, DNS-запити йдуть напряму до роутера, і… Що? Тупить роутер з відповідями? До нього не доходять запити – іноді губляться?

Що це може бути?

  • локальний firewall на Linux чи роутері?
    • ні – вимикав, проблема залишалась
  • race між кількома локальними DNS-сервісами?
    • виключили вище
  • power management мережевої карти – вона уходить в sleep?
    • маловірогідно, але далі перевіряв і це
  • баг драйвера мережевої карти?
    • можливо, бо проблема з’явилась не так давно, до цього на цьому ноуті і цій системі все працювало без проблем
  • якісь проблеми конкретно з UDP?
    • теж ніж – робив dig +tcp google.com, проблема залишалась
  • відповідь на DNS-запит повертається з іншого IP?
    • екзотична ідея, але як варіант – на роутері кілька мережевих інтерфейсів, об’єднаних в bridge, і – теоретично – роутер може відправити відповідь з іншої
    • але це прям щось дуже неординарне, та і проблема виникала однаково на різних роутерах, і раніше її не було

IPv6 та DNS

Не пам’ятаю чому, але десь на початку грішив на IPv6 під час виконання DNS.

/etc/gai.conf керує алгоритмом вибору адрес у glibc (GAI = getaddrinfo()), і визначає яку адресу (IPv4 чи IPv6) програма, яка робила DNS-запит вибере першою у випадку, якщо DNS повернув і A, і AAAA записи.

Можна включити IPv4 first – розкоментувати строку:

...
precedence ::ffff:0:0/96 100
...

Перевіряємо, що повернеться першим – адреса IPv4, чи IPv6:

$ getent ahosts google.com
142.250.130.100 STREAM google.com
...  
2a00:1450:4025:800::64 STREAM 
...

Першим IPv4, але теж не допомогло.

Пробував виключити в ядрі IPv6 взагалі:

$ sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
$ sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1

Тут здавалось, що проблема знайдена – бо перший раз все пройшло без проблем, але ні – потім знов таймаути.

NIC Offloading

NIC Offloading – це коли частина операцій виконується на самому мережевому інтерфейсі, тобто offload деяких задач з CPU ноутбука на контролер карти.

Перевіряємо активні з ethtool -k:

$ sudo ethtool -k enp0s31f6 | grep on
rx-checksumming: on
tx-checksumming: on
        tx-checksum-ip-generic: on
scatter-gather: on
        tx-scatter-gather: on
tcp-segmentation-offload: on
...
generic-segmentation-offload: on
generic-receive-offload: on
rx-vlan-offload: on
tx-vlan-offload: on
receive-hashing: on
...

Самі цікаві тут:

  • TSO (TCP Segmentation Offloading): процесор віддає карті один великий шматок даних (наприклад, 64 КБ), а карта сама “нарізає” його на маленькі TCP-пакети по 1500 байт
  • GSO (Generic Segmentation Offloading): те саме, що й TSO, але більш універсальне (працює не лише для TCP)
  • GRO (Generic Receive Offloading): зворотний процес – карта отримує багато дрібних пакетів, “склеює” їх в один великий і лише тоді віддає процесору, що економить ресурси CPU
  • RX та TX Checksum Offloading: карта сама перевіряє контрольні суми (CRC) вхідних пакетів – якщо пакет “битий”, карта його просто викидає, навіть не повідомляючи операційну систему

По черзі вимикаємо їх, і перевіряємо:

  • sudo ethtool -K enp0s31f6 gro off: не допомогло
  • sudo ethtool -K enp0s31f6 gso off: не допомогло
  • sudo ethtool -K enp0s31f6 tso off: не допомогло
  • sudo ethtool -K enp0s31f6 rx off: не допомогло, і стало навіть гірше

Насправді те, що після відключення RX Checksum Offloading стало гірше – вже було підказкою: якщо до цього мережева карта сама фільтрувала помилки, то тепер вони всі повалили до ядра, що створило додаткове навантаження і хаос у черзі пакетів, тому корисні DNS-відповіді стали губитися ще частіше.

NIC Power Management

EEE (Energy Efficient Ethernet) має зменшувати витрати енергії на роботу карти.

Перевіряємо:

$ sudo ethtool --show-eee enp0s31f6
EEE settings for enp0s31f6:
enabled - active
17 (us)
        Supported EEE link modes:  100baseT/Full
                                   1000baseT/Full
        Advertised EEE link modes:  100baseT/Full
                                    1000baseT/Full
        Link partner advertised EEE link modes:  100baseT/Full
                                                 1000baseT/Full

Зараз “enabled – active” – вимикаємо:

$ sudo ethtool --set-eee enp0s31f6 eee off

Не допомогло.

Ще пробував так: запускаємо ping з короткими інтервалами, аби карта не засинала:

$ ping -i 0.2 192.168.0.1

І одночасно запускаємо цикл з dig – але проблема залишається.

Окремо перевіряв налаштування Runtime Power Management:

Знаходимо адресу PCI для девайсу enp0s31f6:

$ ls -l /sys/class/net/enp0s31f6/device
lrwxrwxrwx 1 root root 0 Jan 19 09:38 /sys/class/net/enp0s31f6/device -> ../../../0000:00:1f.6

Або:

an 19 09:38 /sys/class/net/enp0s31f6/device -> ../../../0000:00:1f.6
[setevoy@setevoy-work ~]  $ lspci -D | grep Ethernet
0000:00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection (18) I219-LM (rev 20)

І перевіряємо параметри power:

$ cat /sys/bus/pci/devices/0000:00:1f.6/power/control
on

on” – включена постійно, значить не має вимикатись.

Драйвер та Message Signaled Interrupts

Перевіряв драйвер:

$ lspci -k -s 00:1f.6
00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection (18) I219-LM (rev 20)
        Subsystem: Lenovo Device 2327
        Kernel driver in use: e1000e
        Kernel modules: e1000e

Мережевий контролер – Intel I219-LM, і драйвер e1000e, про який пишуть, що він “капризний”.

Параметри interrupts:

$ cat /proc/interrupts | grep -i enp0s31f6 ... IR-PCI-MSI-0000:00:1f.6 0-edge enp0s31f6

IR-PCI-MSI-0000:00:1f.6 – драйвер використовує MSI (Message Signaled Interrupts), яка начебто на Linux може давати drops для UDP на деяких картах Intel.

Створив файл /etc/modprobe.d/e1000e.conf, задав interrupt mode в legacy (див. Linux* Driver for Intel(R) Ethernet Network Connection):

options e1000e IntMode=0

Ребутнувся, перевірив:

$ cat /proc/interrupts | grep -i enp0s31f6
  19:     240716         ...  IR-IO-APIC   19-fasteoi   enp0s31f6

Не допомогло – проблема все ще залишалась.

Та і dig +tcp google.com все одно працював з проблемами.

Final: rx_crc_errors та зменшення швидкості

Ну і те, що спочатку пропустив – перевірка помилок на інтерфейсі.

Пропустив, бо кількість помилок не росла під час тестів:

$ ip -s link show enp0s31f6
3: enp0s31f6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether c4:c6:e6:e7:e4:26 brd ff:ff:ff:ff:ff:ff
    RX:  bytes packets errors dropped  missed   mcast           
     750558152  589207    104       0       0       0 
    TX:  bytes packets errors dropped carrier collsns           
      40067575  157761      0       2       0       0 
    altname enxc4c6e6e7e426

Або з ethtool:

$ sudo ethtool -S enp0s31f6 | grep -E "errors|missed|dropped|timeout|tx_aborted" | grep -v ": 0"
     rx_errors: 114
     tx_dropped: 26
     rx_crc_errors: 57

rx_crc_errors каже, що проблема з цілісністю пакетів, і – якщо з роутерам і кабелем все в порядку (а проблема спостерігалась на різних роутерах і з різними кабелями) – то скоріш за все проблема в самому RJ-45 на ноутбуці, хоча контакти виглядають нормально.

Спробував примусово зменшити швидкість на інтерфейсі з гігабіта до 100 Mbps:

$ sudo ethtool -s enp0s31f6 speed 100 duplex full autoneg on

І чудо! Все працює!

Повертаємо знов 1000:

$ sudo ethtool -s enp0s31f6 speed 1000 duplex full autoneg on

І проблема знову з’являється.

Можна було б просто залишити 100 Mbps – але ж я не для того підключений по кабелю і плачу за гігабітний GPON?

Благо, вдома є кілька USB-адаптерів з Ethernet, перемкнув кабель на нього:

$ ip a s enp0s13f0u2u3
2: enp0s13f0u2u3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether c8:4d:44:29:27:6b brd ff:ff:ff:ff:ff:ff
    altname enxc84d4429276b
    inet 192.168.0.198/24 brd 192.168.0.255 scope global dynamic noprefixroute enp0s13f0u2u3
...

Є гігабіт і Full Duplex:

$ sudo ethtool enp0s13f0u2u3
Settings for enp0s13f0u2u3:
        Supported ports: [ TP    MII ]
        Supported link modes:   10baseT/Half 10baseT/Full
                                100baseT/Half 100baseT/Full
                                1000baseT/Half 1000baseT/Full
        ...
        Speed: 1000Mb/s
        Duplex: Full
        ...
                               drv probe link timer ifdown ifup rx_err tx_err tx_queued intr tx_done rx_status pktdata hw wol
        Link detected: yes

І тепер все працює без проблем.

Loading

FreeBSD: Home NAS, part 8 – backup даних NFS та Samba з restic
5 (1)

5 Січня 2026

Власне, по налаштуванню NAS вже зроблено майже все – з VPN є доступи з різних мереж, є різні шари, трохи затюнили безпеку.

Залишилось дві основні речі: моніторинг і бекапи, бо мати ZFS mirror на двох дисках з регулярними ZFS snapshots це, звісно, класно, але все одно недостатньо, а тому хочеться додатково робити бекапи десь в клауд.

Особливо я відчув необхідність мати доступ до бекапів в клаудах на початку війни, коли не ясно було де я опинюсь через годину, і чи буде в мене можливість забрати із собою бодай якесь залізо.

Сьогодні подумаємо і заплануємо як робити бекапи з Linux-хостів на NFS share та як робити бекапи даних NFS і Samba на FreeBSD. При чому бекапи хочеться зробити у два незалежних сховища – AWS S3 та Google Disk: S3 буде основним, а Гугл – резервною копією (резервної копії).

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Планування бекапів

Отже, що в мене є:

  • Linux-хости: домашній і робочий ноутбуки – всякі фото, відео, музика, робочі дані, документи, ключі SSH/GPG/etc, конфіги самої системи
  • FreeBSD-хост: тут живуть Samba та NFS shares і маємо системні дані, які треба зберігати

NFS-шари підключаються до Linux-хостів, які сюди будуть робити бекапи, а потім вже з FreeBSD ці бекапи копіюються до AWS S3 та Google Drive.

Якщо це відобразити схематично, то виходить така картина:

FreeBSD backup plan

Якщо з ноутбуками і Linxu все більш-менш просто – бекапимо важливі дані із /home/setevoy, то з FreeBSD краще продумати окремо, бо тут даних більше:

  • /nas/nfs/backups: бекапи Linux-хостів – треба зберігати копії в клауд
  • /nas/smb: тут два датасети – /nas/smb/media зі всякими музикою-фільмами, та /nas/smb/private з приватними даними, обидва теж зберігати в клауд
  • плюс системні бекапи самої FreeBSD

В системний бекап FreeBSD можна включити:

  • /etc та /usr/local/etc: завжди мати копію всіх конфігураційних файлів
  • /root: якщо там файли типу nas-private-pass.key, яким в мене шифрується і монтується один з датасетів
  • /var/db: pkg і jails metadata, sqlite/db файли деяких сервісів

Вибір утиліт для бекапу

Спочатку думав взяти Timeshift, але, як виявилось, він не вміє в remote storage і не може зберігати на NFS.

А тому треба вибирати щось під мої потреби:

  • мультиплатформа – FreeBSD, Linux, Windows
  • вміти в повне і інкрементальне копіювання
  • мати можливість через конфіг-файл задавати список директорій та файлів, які треба бекапити або пропускати
  • must have – CLI
  • опціонально – мати Web або звичайний GUI
  • вмісти працювати як з локальними файловими системами – так і з NFS або Samba, і на додачу – вміти працювати з клаудами
  • коректно працювати з правами доступу та ACL файлової системи, бо файлові системі різні
  • опціонально – вміти шифрувати бекапи

З тих варіантів, що передивився – найбільше сподобався restic, тож вирішив спробувати його.

Restic overview

Вся його документація – Restic Documentation та Manual.

Основні плюшки restic:

  • написаний на Golang, див GitHub restic
  • доволі простий синтаксис команд для CLI
  • шифрування та data compression з коробки
  • інкрементальні бекапи через власні снапшоти
  • з коробки вміє працювати з NFS та AWS S3, див. Storage Backends.
  • може працювати з іншими бекендами через rclone, а вже rclone має просто безліч варіантів бекендів (див. Supported providers)
  • є third-party Web UI – Backrest
  • є клієнти під всі системи – Linux, FreeBSD, macOS, Windows (див. Installation)

Встановлюємо на Arch Linux:

$ sudo pacman -S restic

Та на FreeBSD:

# pkg install restic

Приклади в цьому пості будуть з Linux, але принципової різниці нема – клієнт працює однаково на всіх платформах.

Restic repositories та snapshots

Документація – Preparing a new repository та Repository Format.

Дані в restic організовані у репозиторіях: кожен репозиторій – це окремий каталог, який містить конфігурацію репозиторію, індекси та зашифровані дані.

Під час створення бекапу restic формує власні логічні snapshots. Дані розбиваються на незалежні блоки (blobs), які і є базовими одиницями зберігання в репозиторії.

При наступних бекапах restic перевіряє, які саме блоки були змінені, і копіює тільки їх, а на блоки даних, які не змінились – створює посилання з нового снапшоту, таким чином оптимізуючи зайнятий дисковий простір.

Тобто тут процес  схожий із ZFS snapshots – тільки у ZFS посилання створюється на блоки самої файлової системи і оригінальні дані, а в restic – на власні блоки даних в каталозі репозиторію.

При цьому restic зберігає дані у власному форматі, а тому ми не залежимо від файлової системи – створюємо бекап з ext4, копіюємо на ZFS, зберігаємо в S3, і відновлюємо на упасі боже Windows з NTFS. Єдине, що нам буде треба – це клієнт restic.

Створюємо тестовий репозиторій:

$ restic init --repo test-repo
enter password for new repository: 
enter password again: 
created restic repository 50ef450308 at test-repo

Перевіряємо зміст:

$ ll test-repo/
total 24
-r--------   1 setevoy setevoy  155 Jan  1 16:47 config
drwx------ 258 setevoy setevoy 4096 Jan  1 16:47 data
drwx------   2 setevoy setevoy 4096 Jan  1 16:47 index
drwx------   2 setevoy setevoy 4096 Jan  1 16:47 keys
drwx------   2 setevoy setevoy 4096 Jan  1 16:47 locks
drwx------   2 setevoy setevoy 4096 Jan  1 16:47 snapshots

Restic та шифрування даних

Документація – Keys, Encryption and MAC.

Кожен репозиторій має ключ, який використовується для доступу до зашифрованих даних:

$ restic key list --repo test-repo
enter password for repository: 
repository 50ef4503 opened (version 2, compression level auto)
 ID        User     Host          Created
-----------------------------------------------------
*0ca1c659  setevoy  setevoy-work  2026-01-01 16:49:49
-----------------------------------------------------

Шифрування даних виконується з master key, який зберігається в репозиторії:

$ restic -r test-repo cat masterkey
enter password for repository: 
repository 50ef4503 opened (version 2, compression level auto)
{
  "mac": {
    "k": "v1PIB3bB1VW46oWWBtKQYA==",
    "r": "Tia4a7HGs7PmN1EzWoWh4g=="
  },
  "encrypt": "0c3l/00P3dTgdbAqPZApAYn7E/MiloqOXyQsYr6AGOA="
}

Для отримання доступу до якого використовуються дані user keys:

$ cat test-repo/keys/0ca***f3a | jq
{
  "created": "2026-01-01T16:49:49.010471345+02:00",
  "username": "setevoy",
  "hostname": "setevoy-work",
  "kdf": "scrypt",
  "N": 32768,
  "r": 8,
  "p": 9,
  "salt": "F3G***50Q==",
  "data": "rjw***FLQ=="
}

Тут:

  • scrypt: KDF (Key Derivation Function), яка використовується для отримання криптографічного ключа з пароля (див. Scrypt Key Derivation Function)
  • salt: сіль з рандомним значенням
  • data: зашифрований master key – саме він використовується для шифрування даних

Коли restic потрібно отримати доступ до даних у репозиторії – він бере введений пароль і salt, передає їх у KDF і формує ключ, який використовується для розшифрування master key репозиторію.

Master key, у свою чергу, застосовується для шифрування та розшифрування ключів, якими вже безпосередньо шифруються дані та метадані в репозиторії.

При цьому можна мати кілька різних user keys (або access keys), які будуть використовуватись для отримання master key.

При потребі пароль можна змінити:

$ restic key passwd --repo test-repo/
enter password for repository: 
repository 50ef4503 opened (version 2, compression level auto)
created new cache in /home/setevoy/.cache/restic
enter new password: 
enter password again: 
saved new key as <Key of setevoy@setevoy-work, created on 2026-01-01 16:49:49.010471345 +0200 EET m=+13.105722833>

При налаштуванні автоматизації бекапів – пароль можна передати зі змінної оточення RESTIC_PASSWORD (див. Environment Variables) або з файлу через --password-file.

Наприклад, для використання паролю з файлу – створимо директорію:

$ mkdir -p ~/.config/restic-test
$ chmod 700 ~/.config/restic-test/

Генеруємо пароль:

$ pwgen 32 1
xoo8eibia2ohch7Oat7zeeshahn0keic

І зберігаємо його в файл ~/.config/restic-test/test-repo-password.

Задаємо доступ на читання тільки власнику:

$ chmod 600 ~/.config/restic-test/test-repo-password

Додаємо новий ключ для репозиторію:

$ restic key add --repo test-repo
enter password for repository: 
repository 50ef4503 opened (version 2, compression level auto)
enter new password: 
enter password again: 
saved new key with ID c08c993b87363c17526e98fd46aeaf14767fa051e3b0d87a32c0cecc50e361d4

Перевіряємо ключі тепер:

[setevoy@setevoy-work ~/Projects/Restic]  $ restic key list --repo test-repo
enter password for repository: 
repository 50ef4503 opened (version 2, compression level auto)
 ID        User     Host          Created
-----------------------------------------------------
*0ca1c659  setevoy  setevoy-work  2026-01-01 16:49:49
 c08c993b  setevoy  setevoy-work  2026-01-01 17:02:10
-----------------------------------------------------

В *0ca1c659 зірочка показує, що зараз репозиторій відкритий з цим ключем.

Пробуємо відкрити з новим ключем – паролем з файла:

$ restic stats --repo test-repo --password-file ~/.config/restic-test/test-repo-password
repository 50ef4503 opened (version 2, compression level auto)
[0:00]          0 index files loaded
scanning...
Stats in restore-size mode:
     Snapshots processed:  0
              Total Size:  0 B

Restic backup та restore

Документація – Backing up.

Для створення бекапів використовуємо команду restic backup, а для відновлення, власне, restic restore.

Бекапимо файл /tmp/restic-test.txt в наш репозиторій:

$ restic backup /tmp/restic-test.txt --repo test-repo
repository 50ef4503 opened (version 2, compression level auto)
no parent snapshot found, will read all files
[0:00]          0 index files loaded

Files:           1 new,     0 changed,     0 unmodified
Dirs:            1 new,     0 changed,     0 unmodified
Added to the repository: 755 B (687 B stored)

processed 1 files, 13 B in 0:01
snapshot bf8def5f saved

Під час кожного виклику restic backup в репозиторії створюється новий snapshot, навіть якщо дані в source не змінювались.

Але, як писав вище – якщо не змінюються дані, то і розмір репозиторію не росте, бо restic просто створить посилання з нового снапшоту на старі блоки даних.

При зміні частини даних – відповідно будуть створені нові блоки тільки для нових даних, на які буде замаплений цей снапшот, а на незмінні дані – в новому снапшоті залишаться старі посилання.

Перевіряємо наявні снапшоти:

$ restic snapshots --repo test-repo
repository 50ef4503 opened (version 2, compression level auto)
ID        Time                 Host          Tags        Paths                 Size
-----------------------------------------------------------------------------------
bf8def5f  2026-01-01 17:07:53  setevoy-work              /tmp/restic-test.txt  13 B
-----------------------------------------------------------------------------------
1 snapshots

Основні корисні команди при роботі з репозиторіями та снапшотами:

  • restic stats: статистика по репозиторію або снапшоту
  • restic check: перевірка цілісності репозиторію
  • restic ls: подивитись зміст снапшоту
  • restic diff: порівняти дані у двох снапшотах
  • restic copy: скопіювати зміст одного репозиторію в інший

І окремо варто згадати --dry-run – перевірити що саме буде виконуватись, і яких даних торкнеться операція.

Для відновлення даних з бекапу використовуємо restic restore і вказуємо ID снапшоту та куди його відновити.

Якщо в destination каталогу нема – restic його створить, а в ньому відновить ієрархію каталогів та файлів зі снапшоту:

$ restic restore --repo test-repo bf8def5f --target /tmp/test-restic-restore
...
restoring snapshot bf8def5f of [/tmp/restic-test.txt] at 2026-01-01 17:07:53.016664301 +0200 EET by setevoy@setevoy-work to /tmp/test-restic-restore
Summary: Restored 2 files/dirs (13 B) in 0:00

Перевіряємо:

$ tree /tmp/test-restic-restore
/tmp/test-restic-restore
└── tmp
    └── restic-test.txt

Include та exclude для backup та restore

При створенні бекапу з restic backup ми вказуємо шлях, який бекапиться, а тому окремої опції --include нема.

Але є --exclude, з яким можна вказати які дані не треба включати в снапшот.

Наприклад, маємо каталог:

$ tree /tmp/restic-dir-test
/tmp/restic-dir-test
├── a.txt
└── sub
    └── b.txt

Бекапимо весь цей каталог, але пропускаємо файл a.txt:

$ restic backup /tmp/restic-dir-test --exclude /tmp/restic-dir-test/a.txt --repo test-repo

Дивимось в снапшоті – a.txt нема:

$ restic ls dfd8271d --repo test-repo
...
/tmp
/tmp/restic-dir-test
/tmp/restic-dir-test/sub
/tmp/restic-dir-test/sub/b.txt

Для restic restore можемо вказати як --include – що саме відновити, так і --exclude – які дані зі снапшота пропустити.

Замість передачі include/exclude в CLI можна всі шляхи описати в файлах, по одному на строку, див. Including Files та Excluding Files.

У файлах можна використовувати коментарі та пусті строки, наприклад, файл backup-nfs.list:

# MAIN
/home/setevoy/Photos
...

# dotdirs
/home/setevoy/.aws
...

Потім викликаємо як:

  • restic backup --files-from backup-nfs.list -r <REPO_NAME>
  • restic backup --files-from backup-nfs.list --exclude-file exclude-nfs.list -r <REPO_NAME>

І аналогічно з restic restore та --include-file і --exclude-file.

Крім того, в include та exclude можна використовувати globbing (не regex):

  • *: – будь-яка послідовність символів в межах одного рівня каталогу
    • приклад: *.log, cache/*
  • **: будь-яка кількість каталогів рекурсивно
    • приклад: **/data, /home/**/cache
  • ?: один будь-який символ
    • приклад: file?.txt
  • [abc]: один символ з набору
    • приклад: file[12].txt
  • [a-z]: діапазон символів
    • приклад: img[0-9].jpg
  • !pattern: інверсія правила (тільки у файлах include/exclude)

Теги для снапшотів

При створенні снапшотів в ZFS ми можемо вказати його ім’я через @.

В restic снапшоти зберігаються тільки з ID – але до них можна додати теги:

$ restic backup /tmp/restic-dir-test --repo test-repo --tag "daily" --tag "$(date +"%Y-%m-%d-%H-%M")"

Далі по цим тегам можна виконувати пошук, copy, restore, forget (про forget далі).

Наприклад, вивести тільки снапшоти з тегом daily:

$ restic snapshots --tag daily -r test-repo
repository 50ef4503 opened (version 2, compression level auto)
ID        Time                 Host          Tags                    Paths                 Size
-----------------------------------------------------------------------------------------------
d14ecde9  2026-01-04 15:08:41  setevoy-work  daily,2026-01-04-15-08  /tmp/restic-dir-test  18 B
-----------------------------------------------------------------------------------------------

Видалення снапшотів з forget та prune

Документація – Removing backup snapshots.

Для очистки даних restic використовуємо команди forget та prune:

  • restic forget: знімає посилання снапшота на блоки даних – але не видаляє їх
  • restic prune: видаляє самі дані – блоки, на які нема посилань зі снапшотів

Наприклад:

$ restic forget f3ce1ac3 -r test-repo
repository 50ef4503 opened (version 2, compression level auto)
[0:00] 100.00%  1 / 1 files deleted

Або відразу виконати prune через restic forget --prune:

$ restic forget 45e6f909 -r test-repo --prune
repository 50ef4503 opened (version 2, compression level auto)
[0:00] 100.00%  1 / 1 files deleted
1 snapshots have been removed, running prune
loading indexes...
[0:00] 100.00%  7 / 7 index files loaded
loading all snapshots...
finding data that is still in use for 5 snapshots
[0:00] 100.00%  5 / 5 snapshots
...
repacking packs
[0:00] 100.00%  1 / 1 packs repacked
rebuilding index
[0:00] 100.00%  8 / 8 indexes processed
[0:00] 100.00%  8 / 8 old indexes deleted
removing 2 old packs
[0:00] 100.00%  2 / 2 files deleted
done

Замість snapshot ID можна вказати policy, див. Removing snapshots according to a policy.

Наприклад:

$ restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --tag daily --prune -r test-repo
repository 50ef4503 opened (version 2, compression level auto)
Applying Policy: keep 7 daily, 4 weekly, 6 monthly snapshots
keep 1 snapshots:
ID        Time                 Host          Tags                    Reasons           Paths                 Size
-----------------------------------------------------------------------------------------------------------------
d14ecde9  2026-01-04 15:08:41  setevoy-work  daily,2026-01-04-15-08  daily snapshot    /tmp/restic-dir-test  18 B
                                                                     weekly snapshot
                                                                     monthly snapshot
-----------------------------------------------------------------------------------------------------------------

Тут:

  • --keep-daily 7: залишаємо снапшоти за останні 7 днів
  • --keep-weekly 4: залишаємо снапшоти за останні 4 тижні (по одному snapshot на тиждень)
  • --keep-monthly 6: залишаємо снапшоти за останні 6 місяців (по одному snapshot на місяць)
  • застосовуємо тільки для снапшотів з тегом daily, і відразу видаляємо дані з диску

Restic mount – репозиторій як директорія

Можна змонтувати репозиторій як звичайну папку, монтується тільки в режимі read-only:

$ mkdir /tmp/restic-mounted-test-repo

$ restic mount -r test-repo /tmp/restic-mounted-test-repo
...
Now serving the repository at /tmp/restic-mounted-test-repo
...

І отримуємо доступ до даних в усіх снапшотах:

$ tree /tmp/restic-mounted-test-repo
/tmp/restic-mounted-test-repo
├── hosts
│   └── setevoy-work
│       ├── 2026-01-01T17:07:53+02:00
│       │   └── tmp
│       │       └── restic-test.txt
...
├── ids
│   ├── 0f477146
│   │   └── tmp
│   │       └── restic-dir-test
│   │           └── sub
│   │               └── b.txt
...
├── snapshots
│   ├── 2026-01-01T17:07:53+02:00
│   │   └── tmp
│   │       └── restic-test.txt
...

Але це можливість більше для якогось ручного дебагу-фіксу, а не для автоматизації.

Restic copy – копіювання даних між репозиторіями

Для копіювання одного репозиторію в інший використовуємо restic copy, див. Copying snapshots between repositories.

Наприклад, так можна копіювати бекапи з репозиторіїв у NFS на FreeBSD до репозиторіїв в AWS S3 та Google Drive.

По-дефолту restic copy скопіює всі снапшоти із source repo, але можна вказати які саме снапшоти копіювати.

Створюємо новий пустий репозиторій:

$ restic init -r new-test-repo

Копіюємо один снапшот зі старого репозиторію:

$ restic copy --from-repo test-repo --repo new-test-repo d14ecde9

Або всі снапшоти з тегом daily:

$ restic copy --from-repo test-repo --repo new-test-repo --tag daily

Тепер в новому репозиторії маємо ті самі дані:

 $ restic snapshots -r new-test-repo
repository fc8a407c opened (version 2, compression level auto)
ID        Time                 Host          Tags                    Paths                 Size
-----------------------------------------------------------------------------------------------
7335e7bf  2026-01-04 15:08:41  setevoy-work  daily,2026-01-04-15-08  /tmp/restic-dir-test  18 B
-----------------------------------------------------------------------------------------------

Restic та репозиторій в AWS S3

З S3 все більш-менш аналогічно до роботи з локальними репозиторіями, але є деякі нюанси.

Для аутентифікації restic використовує звичайний механізм – пошук змінних оточення AWS_ACCESS_KEY_ID та AWS_SECRET_ACCESS_KEY, або пошук у файлах ~/.aws/config та ~/.aws/credentials.

Задаємо змінні:

$ export AWS_PROFILE=setevoy
$ export AWS_DEFAULT_REGION=eu-west-1

В AWS створюємо корзину, а потім ініціалізуємо в ній репозиторій, використовуючи формат s3:s3.amazonaws.com/<BUCKET_NAME>/<REPO_NAME>:

$ restic init --repo s3:s3.amazonaws.com/test-restic-repo-bucket/test-restic-repository
created restic repository 58303a9c88 at s3:s3.amazonaws.com/test-restic-repo-bucket/test-restic-repository

Перевіряємо корзину:

$ aws s3 ls s3://test-restic-repo-bucket --recursive
2026-01-04 16:11:28        155 test-restic-repository/config
2026-01-04 16:11:28        457 test-restic-repository/keys/e53...22f

Важливі нюанси, які треба мати на увазі при роботі з S3:

  • видаляти дані з репозиторію restic в AWS S3 можна тільки через restic forget та restic prune
  • використання S3 Lifecycle rules для restic не рекомендується – навіть для зміни storage class

Каталоги (index/, snapshots/, keys/) активно використовуються restic; якщо перенести, наприклад, keys/ у Glacier або Deep Archive – restic може зависати або падати по таймауту, очікуючи доступ до ключів.

Теоретично lifecycle transitions можна застосувати лише до каталогу data/, де зберігаються pack-файли з даними, але якщо потім запустити restic prune – то restic буде потрібен доступ до старих pack-файлів в data/, і, якщо вони знаходяться в Glacier або Deep Archive, операція стане або дуже повільною, або взагалі неможливою

Тому краще просто робити періодичний restic forget і restic prune, та залишити S3 Standart class даних в бакеті.

Restic та Google Drive через rclone

В мене rclone для Google Drive вже налаштований, допишу про нього окремо, вже є в чернетках, бо теж дуже цікава система.

Що ми можемо зробити – це використати rclone як storage backend для роботи з типами storage, яких нема в самому restic.

Але працює ця схема ну дуже повільно (принаймні з Google Drive) – тому її краще використовувати як one time copy, а не для регулярних бекапів.

Документація – rclone serve restic.

Наприклад, є rclone profile:

$ rclone config show
[setevoy-google-drive]
type = drive
...

Через який я можу підключатись до Google Disk:

$ rclone lsd setevoy-google-drive:
           0 2025-04-16 16:32:56        -1 Arch_Old_Music
           0 2025-04-16 16:50:53        -1 Arch_Work_Photos
           0 2020-06-19 22:13:53        -1 BackendParty-2020-06
...

Створюємо там новий каталог restic-rclone-gdrive-repo:

$ rclone mkdir setevoy-google-drive:restic-rclone-gdrive-repo

З rclone serve restic запускаємо локальний HTTP enpoint нового бекенду для restic вказуючи створений вище каталог – це вже буде корінь репозиторію:

$ rclone serve restic setevoy-google-drive:restic-rclone-gdrive-repo
2026/01/04 16:39:34 NOTICE: Google drive root 'restic-rclone-gdrive-repo': Serving restic REST API on [http://127.0.0.1:8080/]

В іншому вікні для restic задаємо змінну нового ендпоінта:

$ export RESTIC_REPOSITORY=rest:http://127.0.0.1:8080/

Виконуємо ініціалізацію цього репозиторію:

$ restic init
enter password for new repository: 
enter password again: 
created restic repository e1a8edaebd at rest:http://127.0.0.1:8080/

Перевіряємо дані в Google Drive:

$ rclone lsd setevoy-google-drive:restic-rclone-gdrive-repo
           0 2026-01-04 16:42:08        -1 data
           0 2026-01-04 16:42:09        -1 index
           0 2026-01-04 16:42:10        -1 keys
           0 2026-01-04 16:42:11        -1 locks
           0 2026-01-04 16:42:12        -1 snapshots

І скопіюємо дані з AWS S3 до репозиторію в Google Drive

Задаємо змінні:

$ export AWS_PROFILE=setevoy
$ export AWS_DEFAULT_REGION=eu-west-1
$ export RESTIC_REPOSITORY=rest:http://127.0.0.1:8080/

Запускаємо restic copy, але тепер для copy вказуємо тільки --from-repo – бо destination у нас вже заданий через $RESTIC_REPOSITORY:

$ restic copy --from-repo s3:s3.amazonaws.com/test-restic-repo-bucket/test-restic-repository
enter password for source repository: 
repository 58303a9c opened (version 2, compression level auto)
created new cache in /home/setevoy/.cache/restic
enter password for repository: 
repository e1a8edae opened (version 2, compression level auto)
created new cache in /home/setevoy/.cache/restic
[0:00]          0 index files loaded
[0:00]          0 index files loaded

Перевіряємо в Google Drive:

$ restic snapshots 
enter password for repository: 
repository e1a8edae opened (version 2, compression level auto)
ID        Time                 Host          Tags                    Paths                 Size
-----------------------------------------------------------------------------------------------
...
bb02e44b  2026-01-04 15:08:41  setevoy-work  daily,2026-01-04-15-08  /tmp/restic-dir-test  18 B
-----------------------------------------------------------------------------------------------

Що треба мати на увазі при роботі restic через clone:

  • не використовувати rclone mount
  • не виконувати запис одночасно з двох restic клієнтів
  • не використовувати одночасно два rclone serve restic для одного репозиторію

Власне, на цьому все.

Залишилось додати автоматизацію запуску бекапів на Linux та FreeBSD, але це вже опишу окремим постом.

На додачу – документація по тюнингу restic: Tuning Backup Parameters.

Loading

TCP/IP: SYN flood атака на сервер RTFM, та “Hacker News hug of death”
5 (2)

2 Січня 2026

Прилітає сьогодні зранку алерт від моніторингу, що блог впав:

Ну, думаю – знов якийсь DDoS, не перший раз.

Investigating the issue

Йду в Cloudflare, вмикаю Under Attack Mode, і починаю розбиратись.

Дивлюсь запити:

Ага, думаю, ващє фігня – з одного IP запити, зараз його забаню, і готово.

Додаю нове правило з Action = Block в Cloudflare Security Rules, і пішов глянути – що з IP такий?

Whois каже, що якийсь хост з DigitalOcean:

$ whois 46.101.201.123
...
inetnum:        46.101.128.0 - 46.101.255.255
abuse-c:        AD10778-RIPE
netname:        DIGITALOCEAN
...

Там часто всякі боти запускаються, нічого незвичного.

Далі, вирішив з nmap глянути що за сервіси є на тому атакуючому хості:

# nmap -sS 46.101.201.123
...
PORT     STATE    SERVICE
22/tcp   open     ssh
80/tcp   open     http
443/tcp  open     https
...

Хм, думаю – дивно, що за бот такий, що має 80 і 443 порти?

Відкриваю https://46.101.201.123 в браузері, і… попадаю на власний блог 🙂

Шта?…

Перевіряю, які IP в мене в DigitalOcean, і:

Тобто – да, 46.101.201.123 – це Droplet IP сервера, на якому хоститься RTFM.

Хоча взагалі на DNS в IN A для rtfm.co.ua використовується DigitalOcean Reserved IP, який можна переключати між дроплетами:

Тобто:

  • NS для rtfm.co.ua – Cloudflare
  • на них IN A 67.207.75.157
  • Droplet IP 46.101.201.123 не вказаний ніде
  • але запити йдуть на нього

Окей…

Тут ще буде окреме питання – чому в CloudFlare показувались запити від 46.101.201.123 – але про це в кінці.

SYN flood та підключення в SYN_RECV 

Пішов глянути що взагалі на сервері в нетворкінгу, які активні конекти?

А там…

Купа підключень в статусі SYN_RECV – класичний SYN flood: клієнт нам відправляє TCP-пакет з флагом SYN, ми йому відповіли з SYN-ACK, і чекаємо на ACK від нього – але він не приходить, а ресурси CPU/RAM сервера на очікування зайняті (див. TCP handshake, нещодавно писав).

Mitigating the issue

Так як підключення йдуть не через CloudFalre – то і його Security Rules нам не допоможуть.

А Network Firewall в Digital Ocean, як і Security Groups в AWS вміють тільки в Allow правила – але не в Deny (в AWS можна зробити Deny через правила у VPC NACL – Network Access Control List).

Linux Kernel TCP tuning

В першу черги тюнимо параметри TCP-стеку ядра:

# sysctl -w net.ipv4.tcp_syncookies=1
net.ipv4.tcp_syncookies = 1
# sysctl -w net.ipv4.tcp_max_syn_backlog=4096
net.ipv4.tcp_max_syn_backlog = 4096
# sysctl -w net.ipv4.tcp_synack_retries=2
net.ipv4.tcp_synack_retries = 2

Тут:

  • net.ipv4.tcp_syncookies: вмикаємо SYN cookies – ядро може не тримати стан TCP-підключення, коли backlog переповнений
  • net.ipv4.tcp_max_syn_backlog: збільшуємо розмір беклогу для SYN/SYN-ACK, аби реальні клієнти не відвалювались
    • дефолт 256
  • net.ipv4.tcp_synack_retries: обмежуємо кількість спроб ядра відповісти на SYN – скільки раз шлемо SYN-ACK, якщо клієнт не повернув ACK
    • дефолт 5

Вже стало краще:

 

Далі можна на DigitalOcean firewall дозволити доступ тільки з мереж Cloudflare – але вони змінюються, а робити якусь авторизацію зараз влом.

Iptables та DROP by Source Address

Можна, звісно, банити атакуючі мережі – на момент перевірки була одна 177.36.16.0/20:

# netstat -anp | grep 46.101.201.123 | grep SYN_RECV
tcp        0      0 46.101.201.123:443      177.36.16.214:15795     SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      177.36.16.152:43548     SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      177.36.17.0:43309       SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      177.36.17.237:47283     SYN_RECV    -

Додаємо правило з DROP і логуванням для перевірки:

# iptables -I INPUT -s 177.36.16.0/20 -j LOG --log-prefix "DROP 177.36.16.0/20 "

Дивимось, чи працює правило:

# journalctl -k | grep "DROP 177.36.16.0/20" | head
Jan 02 08:34:35 setevoy-do-2023-09-02 kernel: DROP 177.36.16.0/20 IN=eth0 OUT= MAC=de:71:8d:d9:82:55:fe:00:00:00:01:01:08:00 SRC=177.36.16.242 DST=46.101.201.123 LEN=52 TOS=0x00 PREC=0x00 TTL=54 ID=40235 DF PROTO=TCP SPT=17587 DPT=443 WINDOW=65535 RES=0x00 SYN URGP=0 
Jan 02 08:34:37 setevoy-do-2023-09-02 kernel: DROP 177.36.16.0/20 IN=eth0 OUT= MAC=de:71:8d:d9:82:55:fe:00:00:00:01:01:08:00 SRC=177.36.16.193 DST=46.101.201.123 LEN=52 TOS=0x00 PREC=0x00 TTL=56 ID=8752 DF PROTO=TCP SPT=45940 DPT=443 WINDOW=65535 RES=0x00 SYN URGP=0 
...

Залишаємо правило, але прибираємо запис в лог, бо це зайве навантаження на диск і систему:

# iptables -R INPUT 1 -s 177.36.16.0/20 -j DROP

Вже краще – але, очікувано, пішли підключення з інших адрес:

# netstat -anp | grep 46.101.201.123 | grep SYN_RECV
tcp        0      0 46.101.201.123:443      45.94.171.239:48242     SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      146.103.26.224:30654    SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      91.124.63.174:45287     SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      194.116.228.226:15311   SYN_RECV    -

Iptables та DROP by Destination Address

Ну і насправді тут є дуже просте рішення:

  • валідні запити мають йти тільки на Reserved IP, який вказаний в Cloudflare DNS
  • запити на Droplet IP на порт 443 взагалі не мають приходити

Тому просто банимо їх з iptables:

# iptables -A INPUT -p tcp -d 46.101.201.123 --dport 443 -j DROP

Тепер жодного SYN_RECV не залишилось.

Зберігання правил з iptables-persistent

Перевіряємо правила зараз:

# iptables -L INPUT -n --line-numbers
Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination         
1    DROP       0    --  177.36.16.0/20       0.0.0.0/0           
2    DROP       6    --  0.0.0.0/0            46.101.201.123       tcp dpt:443

Аби зберігати їх при ребутах системи – встановлюємо iptables-persistent:

# apt install iptables-persistent

Під час установки він запропонує зберегти правила в файл /etc/iptables/rules.v4:

Перевіряємо що там:

# cat /etc/iptables/rules.v4 | grep 46.101.201.123
-A INPUT -d 46.101.201.123/32 -p tcp -m tcp --dport 443 -j DROP

Готово.

Але це буде працювати, поки не почнуть флудити на сам Reserved IP 67.207.75.157.

Тоді вже доведеться робити через дозвіл тільки з Cloudflare IPs.

Bonus: WordPress, Cloudflare та запити від Droplet IP

Але після того, як із SYN flood начебто розібрався – графіки кількості запитів в Cloudflare не зменшились.

І це логічно – бо SYN взагалі йшов напряму до сервера на IP 46.101.201.123, а не через Cloudflare, а там ці запити в Clodflare взагалі не трекались.

При цьому в логах Cloudflare від IP 46.101.201.123 всюди був один і той самий Path до файлу /wp-content/uploads/2025/11/freebsd_logo1.jpg:

Графіки Cloudflare за останні 6 годин виглядали так – в топі Source IPs зліва внизу саме 46.101.201.123:

Тут я вже напрягся:

  • SYN flood почався близько 10 ранку за Києвом
  • в цей жеж час є спайк запитів до Cloudflare від самого сервера RTFM

Тобто виглядало так, ніби на сервері якийсь код постійно звертається до одного і того самого URL на самому сервері.

Відключив Cloudflare WordPress плагін – ні, запити не спадають.

Відключив WP_CRON – теж не допомогло.

WTF is going on? – судорожно думав я, і додумався, що пора б включити і подивитись NGINX access logs і подивитись, що взагалі на сервер приходить.

А access logs побачив купу записів виду:

...
[02/Jan/2026:11:07:35 +0000] "GET /en/freebsd-home-nas-part-1-configuring-zfs-mirror-raid1/ HTTP/1.1" 200 35451 "-" "HackerNews/1536 CFNetwork/3860.200.71 Darwin/25.1.0"
[02/Jan/2026:11:07:42 +0000] "GET /en/freebsd-home-nas-part-1-configuring-zfs-mirror-raid1/ HTTP/1.1" 200 35462 "https://news.ycombinator.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36"
[02/Jan/2026:11:07:43 +0000] "GET /wp-json/pvc/v1/increase/33806 HTTP/1.1" 200 99 "https://rtfm.co.ua/en/freebsd-home-nas-part-1-configuring-zfs-mirror-raid1/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36"
...

HackerNews, ycombinator? Вау…

А друге – сама причина 46.101.201.123 в логах Clodflare: “GET /wp-json/pvc/v1/increase/” – це запит до WordPress-плагіна Page View Count, який не так давно включив. А “https://rtfm.co.ua/en/freebsd-home-nas-part-1-configuring-zfs-mirror-raid1″ – звідки запит був зроблений.

Тобто, плагін на сторінці поста робить запит, аби збільшити лічильник переглядів – при цьому підставляючи Referrer у вигляді тої сторінки, звідки він запит робить.

Cloudflare жеж бачить, що запит йде від Origin – і використовує в логах Droplet IP.

Ну а далі вже проста перевірка – що коїться взагалі з постом FreeBSD: Home NAS, part 1 – configuring ZFS mirror (RAID1):

Це при тому, що зазвичай переглядів кілька десятків, ну максимум 100-200.

І перевірка в гуглі вже показала причину такого напливу:

А трапилось те, що я сьогодні вранці перший раз запостив лінк на https://lobste.rs, звідки його перепостили на Hacker News, і я отримав “Hacker News hug of death” – див. Surviving the Hug of Death, де у людини була схожа ситуація.

Після відключення плагіну Page View Count – Droplet IP 46.101.201.123 в Cloudflare зник.

Loading

FreeBSD: Home NAS, part 7 – NFSv4 та підключення до Linux
5 (1)

31 Грудня 2025

Наступний крок в процесі налаштування домашнього NAS на FreeBSD – додати NFS share.

Samba зробили в попередній частині – тепер до неї додамо шари з NFS: моя ідея в тому, щоб Samba share використовувалась для всяких медіа-ресурсів, до яких потрібен доступ з телефонів та TV, а NFS буде виключно для Linux-хостів – двох ноутбуків в різних мережах (дім і офіс), які на цей розділ будуть робити свої бекапи з rsync, rclone або restic.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

NFSv3 vs NFSv4

Наразі є дві основні версії NFS – v3 та v4.

FreeBSD підтримує робота з обома (я під час тестів налаштовував і v3 теж), але, очевидно, що v4 більш актуальна, і має низку переваг:

  • NFSv3 – stateless, а NFSv4 – stateful: v4 зберігає стан клієнтів і сесій, що спрощує роботу з блокуваннями та доступом до файлів
  • NFSv3 вважається простішою: але, як на мене, то NFSv4 налаштовується не складніше, і навіть простіше через меншу кількість компонентів
  • аутентифікація:
    • NFSv3 аутентифікація – IP клієнта + UID/GID (хто підключився і від імені якого користувача)
    • NFSv4 – розширена модель доступу, підтримка ACL (і база для Kerberos, якщо потрібно)

Див. NFSv3 and NFSv4: What’s the difference?

Створення ZFS datasets

Аби мати можливість окремих налаштувань снапшотів і ZFS quotes – датасети для NFS зробимо ієрархічно:

  • nas/: корневий датасет ZFS-пулу
    • nas/nfs/: корневий датасет для всього NFS
      • nas/nfs/backups/: датасет для бекапів з інших машин

А в nas/nfs/backups/ вже будуть окремі каталоги з іменами хостів для їхніх бекапів – “setevoy-home“, “setevoy-work“, “setevoy-rtfm” і т.д.

Пізніше, при потребі, в nas/nfs/ можна буде додати нову NFS-шару.

Додаємо новий dataset:

root@setevoy-nas:/home/setevoy # zfs create nas/nfs

В ньому створюємо другий, для бекапів:

root@setevoy-nas:/home/setevoy # zfs create nas/nfs/backups

Сам NFS daemon в системі є з коробки, нам треба тільки додати його запуск:

root@setevoy-nas:/home/setevoy # sysrc nfs_server_enable="YES"
root@setevoy-nas:/home/setevoy # sysrc nfsv4_server_enable="YES"
root@setevoy-nas:/home/setevoy # sysrc nfsv4_server_only="YES"
root@setevoy-nas:/home/setevoy # sysrc nfsuserd_enable="YES"

NFSv4 працює з іменами, а не “сирими” UID/GID, тому, аби ID коректно мапились в імена – додаємо в /etc/sysctl.conf, і активуємо зміни зараз:

root@setevoy-nas:/home/setevoy # sysctl vfs.nfs.enable_uidtostring=1
vfs.nfs.enable_uidtostring: 0 -> 1
root@setevoy-nas:/home/setevoy # sysctl vfs.nfsd.enable_stringtouid=1
vfs.nfsd.enable_stringtouid: 0 -> 1

NFS, ZFS та sharenfs 

ZFS підтримує налаштування NFS для датасетів через dataset property sharenfs (а для Samba – через sharesmb), хоча є має обмеження.

Для NFSv4 обов’язково треба вказати корневу директорію, в якій будуть самі шари, див. NFS Version 4 Protocol.

Додаємо її в файл /etc/exports:

V4: /nas/nfs

Але доступу до цього корневого каталогу не буде, поки ми не додамо його явно, наприклад як:

# zfs set sharenfs="-network 192.168.0.0/24 -ro" nas/nfs

Тепер можемо через sharenfs розшарити nas/nfs/backups датасет, для якого з -network вказуємо з яких адрес буде дозволений доступ:

root@setevoy-nas:/home/setevoy # zfs set sharenfs="-network 192.168.0.0/24" nas/nfs/backups

Перевіряємо:

root@setevoy-nas:/home/setevoy # zfs get sharenfs nas/nfs/backups
NAME             PROPERTY  VALUE                    SOURCE
nas/nfs/backups  sharenfs  -network 192.168.0.0/24  local

По факту, zfs set sharenfs просто редагує файл /etc/zfs/exports, який потім читається mountd:

root@setevoy-nas:~ # cat /etc/zfs/exports
# !!! DO NOT EDIT THIS FILE MANUALLY !!!

/nas/nfs/backups        -network 192.168.0.0/24

Запускаємо nfsd:

root@setevoy-nas:/home/setevoy # service nfsd start

nfsd автоматом запустить mountd, який власне відповідає за NFS sharing і читає конфігурацію з /etc/exports та /etc/zfs/exports:

root@setevoy-nas:~ # ps aux | grep mount
root      9475   0.0  0.0     13888   1076  -  Is   05:46       0:00.00 /usr/sbin/mountd -r -S -R /etc/exports /etc/zfs/exports

Додаємо правило на фаєрволі:

...
pass in on em0 proto { tcp udp } from { 192.168.0.0/24, 192.168.100.0/24, 10.8.0.0/24 } to (em0) port 2049
...

Перевіряємо синтаксис, застосовуємо зміни pf:

root@setevoy-nas:~ # pfctl -nvf /etc/pf.conf && service pf reload

Монтуємо на клієнті – вказуємо явно -t nfs4:

[setevoy@setevoy-work ~] $ sudo mkdir /mnt/test/
[setevoy@setevoy-work ~] $ sudo mount -t nfs4 192.168.0.2:/backups /mnt/test/

В 192.168.0.2:/backups каталог /backups вказуємо від корня, який задали в /etc/exports: тобто корінь у нас “/nas/nfs” – значить на клієнтах він буде “/“, і, відповідно, внутрішні датасети монтуємо від цього корня як /backups.

Перевіряємо маунти на клієнті:

[setevoy@setevoy-work ~]  $ findmnt /mnt/test/
TARGET SOURCE            FSTYPE OPTIONS
/mnt/test
       192.168.0.2:/backups
                         nfs4   rw,relatime,vers=4.2,rsize=131072,wsize=131072,namlen=255,hard,fatal_neterrors=none,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.0.4,local_lock=none,addr

З цікавого для нас тут:

  • nfs4: протокол
  • vers=4.2: версія
  • sec=sys: аутентифікація клієнтів (буде важливо для /etc/fstab на клієнтах, див. в кінці)
  • clientaddr=192.168.0.4: і адреса самого клієнта

Аналогічно на клієнті можна перевірити активне підключення з nfsstat -m:

[setevoy@setevoy-work ~]  $ nfsstat -m
/mnt/test from 192.168.0.2:/backups
 Flags: rw,relatime,vers=4.2,rsize=131072,wsize=131072,namlen=255,hard,fatal_neterrors=none,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.0.4,local_lock=none,addr=192.168.0.2

А на сервері – з nfsdumpstate:

root@setevoy-nas:/home/setevoy # nfsdumpstate
Flags         OpenOwner      Open LockOwner      Lock     Deleg  OldDeleg Clientaddr
                      0         0         0         0         0         0 192.168.0.4

А з опцією -l можна подивитись активні Opens and Locks – але зараз тут пусто.

Перевіряємо доступи – створимо файл на сервері:

root@setevoy-nas:/home/setevoy # touch /nas/nfs/backups/test-server

І дивимось на клієнті:

[setevoy@setevoy-work ~]  $ ll /mnt/test/
total 1
-rw-r--r-- 1 root root 0 Dec 31 13:12 test-server

(Dec 31 – коли б ще сетапити домашній NAS на FreeBSD, правда? 😀 )

NFS, users та “Permission denied”

Але з клієнта ми зараз не можемо нічого писати в каталог:

[setevoy@setevoy-work ~]  $ touch /mnt/test/test-client
touch: cannot touch '/mnt/test/test-client': Permission denied

Бо на сервері він створювався від root:

root@setevoy-nas:~ # stat /nas/nfs/backups/test-server
4446369902026857636 4 -rw-r--r-- 1 root wheel 0 0 "Dec 31 13:12:36 2025" "Dec 31 13:12:36 2025" "Dec 31 13:12:36 2025" "Dec 31 13:12:36 2025" 131072 1 0x800 /nas/nfs/backups/test-server

Ба більше – доступу нема навіть від локального root на клієнті:

[setevoy@setevoy-work ~]  $ sudo touch /mnt/test/test-client
touch: cannot touch '/mnt/test/test-client': Permission denied

А по-дефолту NFS виконує root_squash, і всі операції від клієнтів на сервері виконує від локального юзера nobody.

Є кілька варіантів вирішення:

  • на сервері створити групу типу nfsusers, дати їй права на запис в каталог (775), і додати локального юзера setevoy в цю групу
    • самий кошерний і безпечний варіант
  • або можна задати опцію -maproot=root – тоді root на клієнті буде == root на сервері (UID в обох 0)
    • але це тільки про доступ до файлів, і тільки в межах NFS root – /nas/nfs
    • ОК варіант для домашнього NAS
  • трохи безпечніший варіант – вказати -maproot=setevoy і на сервері змінити власника /nas/nfs/backups/ – тоді операції від root на клієнті на сервері будуть виконуватись від UID/GID юзера setevoy на сервері
  • або взагалі зробити -mapall=root – і тоді всі юзери на клієнті будуть виконувати операції як локальний root
    • аналогічно до -maproot=root, але і самий небезпечний варіант

Так як ця шара для бекапів, які на клієнтах будуть виконуватись від root – то можна використати maproot=root:

root@setevoy-nas:/home/setevoy # zfs set sharenfs="-network 192.168.0.0/24 -maproot=root" nas/nfs/backups
root@setevoy-nas:/home/setevoy # service nfsd restart

Тепер на клієнті від звичайного юзера у нас доступу все ще нема:

[setevoy@setevoy-work ~]  $ touch /mnt/test/test-client
touch: cannot touch '/mnt/test/test-client': Permission denied

Але є від локального root, бо він отримує права root на сервері:

[setevoy@setevoy-work ~]  $ sudo touch /mnt/test/test-client

ZFS sharenfs та muliple -network

Дуже довго проковирявся з цим 🙁

Допомогли на форумі FreeBSD, див. NFSv4 and share for multiply networks (взагалі, FreeBSD community дуже відкрите, і набагато менш токсичне, аніж ті ж форуми Arch Linux).

В чому проблема: в мене доступ до хоста FreeBSD відбувається з кількох мереж (див. FreeBSD: Home NAS, part 3 – WireGuard VPN, Linux peer та routing):

  • 192.168.0.0/24: мережа офісу
  • 192.168.100.0/24:  домашня мережа
  • 10.8.0.0/24: VPN

І, звісно, хочеться доступ до NFS мати захищеним на рівні мереж (хоча в моєму випадку цілком можна було обійтись без цього, але краще відразу робити правильно).

А проблема полягає в том, що у ZFS ver 2.2.7, яка використовується у FreeBSD 14.3 зараз, нема можливості вказати кілька мереж в sharenfs property.

Тобто, не можна використати щось типу:

# zfs set sharenfs="-network 192.168.0.0/24 -network 192.168.100.0/24 -network 10.8.0.0/24" nas/nfs/backups

Але у FreeBSD 15.0 і ZFS 2.4.0 начебто розширили синтаксис, і там вже можна передати список, розділений “;“:

# zfs  sharenfs="-network 192.168.0.0/24 -maproot=root;-network 192.168.100.0/24" nas/nfs/backups

Ну а на версії 2.2.7 – просто робимо шару не через zfs set sharenfs, а напряму у файлі /etc/exports.

Відмонтовуємо шару на клієнті:

[setevoy@setevoy-work ~]  $ sudo umount /mnt/test

На сервері прибираємо sharenfs:

root@setevoy-nas:~ # zfs set sharenfs=off nas/nfs/backups

Перевіряємо:

root@setevoy-nas:~ # zfs get sharenfs nas/nfs/backups
NAME             PROPERTY  VALUE     SOURCE
nas/nfs/backups  sharenfs  off       local

Перевіряємо, що /etc/zfs/exports тепер пустий:

root@setevoy-nas:~ # cat /etc/zfs/exports
# !!! DO NOT EDIT THIS FILE MANUALLY !!!

root@setevoy-nas:~ #

Далі, редагуємо файл /etc/exports, і задаємо шари тут, кожен запис для окремої мережі:

V4: /nas/nfs
/nas/nfs/backups -network 192.168.0.0/24 -maproot=root
/nas/nfs/backups -network 192.168.100.0/24 -maproot=root
/nas/nfs/backups -network 10.8.0.0/24 -maproot=root

Перезапускаємо nfsd та mountd (mountd рестартимо явно вручну, бо були проблеми з доступом і помилки “Input/output error“):

Перевіряємо доступ з клієнта в офісі:

[setevoy@setevoy-work ~]  $ sudo mount -t nfs4 192.168.0.2:/backups /mnt/test
[setevoy@setevoy-work ~]  $ file /mnt/test/test-client
/mnt/test/test-client: empty

І з клієнта, який вдома підключений через VPN:

[setevoy@setevoy-home ~]$ sudo wg show
interface: wg0
...
peer: xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
  endpoint: 178.***.***.184:51830
  allowed ips: 10.8.0.1/32, 192.168.0.0/24

З адресою 10.8.0.3:

[setevoy@setevoy-home ~]$ ip a s wg0
44: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none 
    inet 10.8.0.3/24 
    ...

Встановлюємо nfs-utils, інакше буде помилка “NFS: mount program didn’t pass remote address“:

[setevoy@setevoy-home ~]$ sudo pacman -S nfs-utils

Підключаємо шару на цьому клієнті:

[setevoy@setevoy-home ~]$ sudo mount -t nfs4 192.168.0.2:/backups /mnt/test/

Перевіряємо доступ:

[setevoy@setevoy-home ~]$ sudo touch /mnt/test/test-vpn-client

Перевіряємо:

[setevoy@setevoy-home ~]$ ls -l /mnt/test/test-vpn-client
-rw-r--r-- 1 root root 0 Dec 31 15:16 /mnt/test/test-vpn-client

І на сервері тепер бачимо два активних підключення: один Clientaddr з адреси мережі VPN – 10.8.0.3, і один з офісної – робочий ноут з 192.168.0.4:

root@setevoy-nas:~ # nfsdumpstate
Flags         OpenOwner      Open LockOwner      Lock     Deleg  OldDeleg Clientaddr
CB                    1         0         0         0         0         0 10.8.0.3   
                      0         0         0         0         0         0 192.168.0.4

Linux, /etc/fstab та systemd-automount

І наостанок додамо автомаунт, як це зроблено для Samba share.

Відключаємо шару зараз:

[setevoy@setevoy-work ~] $ sudo umount /mnt/test

Створюємо вже постійний каталог:

[setevoy@setevoy-work ~] $ sudo mkdir /nas/nfs/backups/

На клієнтах редагуємо /etc/fstab:

...

192.168.0.2:/backups  /nas/nfs/backups  nfs  sec=sys,_netdev,noauto,x-systemd.automount,nofail,noatime  0  0

Тут явно вказуємо sec=sys, тобто аутентифікацію за AUTH_SYS (по UID/GID, див. The AUTH_SYS authentication method).

Виконуємо sudo systemctl daemon-reload, перевіряємо нові unit-файли:

[setevoy@setevoy-work ~]  $ ll /run/systemd/generator/ | grep nfs
-rw-r--r-- 1 root root 177 Dec 31 15:29 nas-nfs-backups.automount
-rw-r--r-- 1 root root 274 Dec 31 15:29 nas-nfs-backups.mount

Активуємо їх:

[setevoy@setevoy-work ~]  $ sudo systemctl restart remote-fs.target

І перевіряємо доступ – шара має підключитись автоматично:

[setevoy@setevoy-work ~]  $ ll /nas/nfs/backups
total 2
-rw-r--r-- 1 setevoy nfsusers 0 Dec 31 14:18 test-client
-rw-r--r-- 1 root    root     0 Dec 31 13:12 test-server
-rw-r--r-- 1 root    nfsusers 0 Dec 31 15:16 test-vpn-client

Готово.

Корисні посилання

Loading

Arch Linux: pacman -Syu та помилки “Failed to connect to system scope bus via local transport”
0 (0)

29 Грудня 2025

Дуже рідко бувають проблеми з апгрейдами на Arch Linux, а така ситуація, як сьогодні, в мене за майже 10 років користування системою вперше.

Отже, що трапилось:

  • встановив апгрейди з pacman -Syu
  • після установки sudo reboot зависав
  • ребутнутись вдалось тільки з sudo reboot -f (force)
  • після ребуту – X.Org запустився, SDDM теж, Plasma теж – але на робочому столі все зависало і не реагувало на клаву і мишку

Дичина якась 🙂

Пофіксити вдалось, хоча так і не зрозумів що саме і чому впало.

Проблема : sudo reboot та “Failed to connect to system scope bus via local transport”

Це насправді було першим дзвіночком.

В логах при виконанні sudo reboot з’являлось повідомлення про:

...
Call to Reboot failed: Connection timed out
Failed to connect to system scope bus via local transport: Connection refused
...

Явно проблема з D-Bus/systemd, і саме тому допомогло sudo reboot -f (чи з sudo shutdown -r now, щось з них двох спрацювало), бо в такому випадку systemd не намагається координувати shutdown через system bus і напряму викликає примусовий перезапуск.

Окей – ребутнулись, і тут виникла друга – сама цікава – проблема.

KDE Plasma “висить” після апгрейду і ребуту

Беру в лапки “Plasma висить”, бо проблема все ж була не в ній – але проявлялось саме там.

Отже, ребунувся, залогінився, робочий стіл і сервіси запустились:

Але на цьому – все.

Ніякої реакції на клаву і мишку.

Перша підозра – проблема з KDE Wallet або GNOME Keyring – бо все виглядало так, що зависання виникає після їх старту.

При цьому SSH працював, і я зміг підключатись з іншого ноутбука аби подебажити і пофіксити.

Fix attempt : очистка Plasma та kwalletd

Перша спроба фіксу – прибрати все, що відноситься до Plasma і KWallet:

$ mv .cache/ .cache.bak 
$ mv .config/kwalletrc .config/kwalletrc.bak 
$ mkdir .config/plasma-bak 
$ mv .config/plasma* .config/plasma-bak/
$ mv .local/share/kwalletd/ .local/share/kwalletd.bak
$ sudo reboot

Але після ребуту і логіну система все ще висить і не реагує.

Fix attempt #2: запуск чистого X11

Далі спробував логін з чистим X11 замість Plasma – по SSH встановив:

$ sudo pacman -S --needed xorg-server xorg-xinit xterm

Хоча xorg-server і xorg-xinit вже були, бо я на X11, а не глючному Wayland.

Далі по SSH прибив поточну сесію:

$ sudo systemctl stop sddm

На ноутбуці через Alt + F2 зайшов в консоль (до речі… дивно, що це спрацювало, але ок), і запустив startx.

X11 та xterm завантажились – і тут жеж все знов перестало реагувати на клаву-мишку.

Краса…)

А отже, проблема виникає не на рівні Plasma чи конкретного desktop environment, а нижче – на рівні ядра, systemd, input stack та їх ініціалізації.

Fix attempt #3: pacman.log та “Failed to connect to system scope bus via local transport: Connection refused”

Окей – пішов дивитись логи pacman, і ось тут було дещо цікаве:

[2025-12-29T09:33:25+0200] [PACMAN] Running 'pacman -Syu --noconfirm'
[2025-12-29T09:33:25+0200] [PACMAN] synchronizing package lists
[2025-12-29T09:33:28+0200] [PACMAN] starting full system upgrade
...
[2025-12-29T09:33:49+0200] [ALPM] upgraded systemd (258.3-1 -> 259-1)
[2025-12-29T09:33:49+0200] [ALPM-SCRIPTLET] Creating group 'empower' with GID 946.
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET] :: This is a systemd feature update. You may want to have a look at
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET]    NEWS for what changed, or if you observe unexpected behavior:
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET]      /usr/share/doc/systemd/NEWS
[2025-12-29T09:35:20+0200] [ALPM] upgraded polkit (126-2 -> 127-2)
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
...
[2025-12-29T09:35:26+0200] [ALPM] running '30-systemd-catalog.hook'...
[2025-12-29T09:35:26+0200] [ALPM] running '30-systemd-daemon-reload-system.hook'...
[2025-12-29T09:35:26+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
[2025-12-29T09:35:26+0200] [ALPM] running '30-systemd-daemon-reload-user.hook'...
[2025-12-29T09:35:27+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
[2025-12-29T09:35:27+0200] [ALPM] running '30-systemd-hwdb.hook'...
[2025-12-29T09:35:27+0200] [ALPM] running '30-systemd-restart-marked.hook'...
[2025-12-29T09:35:27+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
....
[2025-12-29T09:35:30+0200] [ALPM-SCRIPTLET] ==> Building image from preset: /etc/mkinitcpio.d/linux.preset: 'default'
[2025-12-29T09:35:30+0200] [ALPM-SCRIPTLET] ==> Using default configuration file: '/etc/mkinitcpio.conf'
[2025-12-29T09:35:30+0200] [ALPM-SCRIPTLET]   -> -k /boot/vmlinuz-linux -g /boot/initramfs-linux.img
...
[2025-12-29T09:35:33+0200] [ALPM-SCRIPTLET] ==> Creating zstd-compressed initcpio image: '/boot/initramfs-linux.img'
[2025-12-29T09:35:33+0200] [ALPM-SCRIPTLET]   -> Early uncompressed CPIO image generation successful
[2025-12-29T09:35:33+0200] [ALPM-SCRIPTLET] ==> Initcpio image generation successful
[2025-12-29T09:35:33+0200] [ALPM] running 'dbus-reload.hook'...
[2025-12-29T09:35:33+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
...

І згадуємо першу проблему – що нормальний reboot не працював з тою самою помилкою підключення до системної шини (через /run/dbus/system_bus_socket).

І система не змогла виконати всі hooks, і в результаті після рестарту частина системних сервісів зависала.

Найімовірніше, це торкнулось компонентів, які залежать від udev, logind та input stack. В результаті X.Org і display manager стартували, але пристрої вводу (keyboard/mouse) опинились у неконсистентному стані, без коректної обробки подій.

Final fix: установка ядра linux-lts (але діло не в LTS)

Тут в мене з’явилась підозра, що проблема в ядрі і Intel-дайверах, і вирішив спробувати встановити LTS-ядро:

$ pacman -S linux-lts linux-lts-header

Правда, після встановлення забув зробити grub-mkconfig, а тому в меню GRUB “Advanced options for Arch Linux” нове ядро не з’явилось, і я просто загрузився зі старим.

Але… Саме після цього все запрацювало.

Чому – бо під час встановлення LTS заново запустились всі хуки, бо перший апдейт відбувався в момент оновлення systemd і D-Bus, а другий – у вже стабільному systemd-оточенні:

...
[2025-12-29T10:40:15+0200] [PACMAN] Running 'pacman -S linux-lts linux-lts-headers'
[2025-12-29T10:40:21+0200] [ALPM] transaction started
[2025-12-29T10:40:21+0200] [ALPM] installed linux-lts (6.12.63-1)
[2025-12-29T10:40:21+0200] [ALPM] installed pahole (1:1.31-1)
[2025-12-29T10:40:21+0200] [ALPM] installed linux-lts-headers (6.12.63-1)
[2025-12-29T10:40:21+0200] [ALPM] transaction completed
[2025-12-29T10:40:22+0200] [ALPM] running '30-systemd-update.hook'...
[2025-12-29T10:40:22+0200] [ALPM] running '60-depmod.hook'...
[2025-12-29T10:40:23+0200] [ALPM] running '90-mkinitcpio-install.hook'...
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET] ==> Building image from preset: /etc/mkinitcpio.d/linux-lts.preset: 'default'
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET] ==> Using default configuration file: '/etc/mkinitcpio.conf'
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> -k /boot/vmlinuz-linux-lts -g /boot/initramfs-linux-lts.img
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET] ==> Starting build: '6.12.63-1-lts'
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [base]
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [udev]
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [autodetect]
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [microcode]
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [modconf]
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [kms]
[2025-12-29T10:40:24+0200] [ALPM-SCRIPTLET]   -> Running build hook: [keyboard]
[2025-12-29T10:40:24+0200] [ALPM-SCRIPTLET]   -> Running build hook: [keymap]
[2025-12-29T10:40:24+0200] [ALPM-SCRIPTLET]   -> Running build hook: [consolefont]
[2025-12-29T10:40:24+0200] [ALPM-SCRIPTLET] ==> WARNING: consolefont: no font found in configuration
[2025-12-29T10:40:24+0200] [ALPM-SCRIPTLET]   -> Running build hook: [block]
[2025-12-29T10:40:25+0200] [ALPM-SCRIPTLET]   -> Running build hook: [encrypt]
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET]   -> Running build hook: [resume]
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET]   -> Running build hook: [filesystems]
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET]   -> Running build hook: [fsck]
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET] ==> Generating module dependencies
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET] ==> Creating zstd-compressed initcpio image: '/boot/initramfs-linux-lts.img'
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET]   -> Early uncompressed CPIO image generation successful
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET] ==> Initcpio image generation successful
...

І після цього система запрацювала нормально.

Коротше – класичний приклад неочевидної “магії” systemd, D-Bus та системних hooks під час апгрейду.

Lessons learned

  • якщо під час pacman -Syu з’являються помилки systemd/dbus, їх не варто ігнорувати – а я звик, що апгрейди проходять без проблем, і не подивився на pacman output в консолі, а відразу почав ребутати машину
  • зависання GUI не завжди означає проблему в Destop Environment або Wayland/X11
  • повторний запуск системних hooks у стабільному середовищі може повністю виправити систему

Loading

SSH: sshd hardening на FreeBSD і Linux, та інтеграція з 1Password
0 (0)

28 Грудня 2025

Прийшов час трохи привести в порядок SSH на самому FreeBSD та на клієнтах – ноутбуках з Arch Linux, бо на домашніх машинках досі використовую парольну аутентифікацію.

Власне, описані нижче налаштування не специфічні ні для FreeBSD, ні для Linux, бо SSH server один і той самий на всіх системах (OpenSSH_9.9p2 на FreeBSD 14.3 і OpenSSH_10.2p1 на Arch Linux).

1Password користуюсь вже давно, класна інтеграція і GUI, хоча паралельно є локальний KeePassXC, в який до окремої бази періодично бекаплю дані із 1Password – див. How to export your data from the 1Password desktop app та Migrating from 1Password to KeePass, KeePassXC and KeePassium.

Це пост – частина серії по сетапу домашнього NAS на FreeBSD (див. початок у FreeBSD: Home NAS, part 1 – налаштування ZFS mirror), але вирішив його зробити окремою темою, бо тут виключно про SSH.

Що маю в поточному сетапі:

  • хост з FreeBSD/NAS: доступний тільки в локальній мережі і VPN, тому SSH brute force не очікується (але при параної або на публічно доступних серверах можна додати Fail2Ban чи SSHGuard)
  • pf firewall: як перша лінія захисту з лімітом того, звідки SSH доступний (див. FreeBSD: Home NAS, part 2 – знайомство з Packet Filter (PF) firewall)
  • sshd: друга лінія захисту, базові налаштування доступу

SSH та аутентифікація по ключам

Перше і основне – це налаштувати доступ по ключам замість парольної аутентифікації.

Зробимо це, потім доступ по паролям відключимо взагалі.

На клієнті, ноутбуці з Linux, створюємо ключі:

[setevoy@setevoy-work ~] $ ssh-keygen -t ed25519 -C "setevoy@setevoy"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/setevoy/.ssh/id_ed25519): /home/setevoy/.ssh/freebsd-nas
Enter passphrase for "/home/setevoy/.ssh/freebsd-nas" (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/setevoy/.ssh/freebsd-nas
Your public key has been saved in /home/setevoy/.ssh/freebsd-nas.pub

Копіюємо на хост з FreeBSD:

[setevoy@setevoy-work ~] $ ssh-copy-id -i /home/setevoy/.ssh/freebsd-nas [email protected]
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/setevoy/.ssh/freebsd-nas.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
([email protected]) Password for setevoy@setevoy-nas:

Number of key(s) added: 1

Now try logging into the machine, with: "ssh -i /home/setevoy/.ssh/freebsd-nas '[email protected]'"
and check to make sure that only the key(s) you wanted were added.

Перевіряємо файл ~/.ssh/authorized_keys на FreeBSD у юзера setevoy:

root@setevoy-nas:/home/setevoy # cat .ssh/authorized_keys 
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILb2zkJzflngGx0qY71xYyHVvKI8A2GTAGTqppS0yVz2 setevoy@setevoy

Пробуємо підключення з Linux:

[setevoy@setevoy-work ~]  $ ssh -i /home/setevoy/.ssh/freebsd-nas '[email protected]'
Last login: Sat Dec 27 09:01:47 2025 from 192.168.0.4
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!

...
[setevoy@setevoy-nas ~]$ 

Все працює – можна налаштувати SSH Agent.

FreeBSD та 1Password client

Тут просто для приклада – установка і підключення клієнта 1Password на FreeBSD.

Встановлюємо:

root@setevoy-nas:/home/setevoy # pkg install -y 1password-client2

Додаємо акаунт:

root@setevoy-nas:/home/setevoy # op account add
Enter your sign-in address (example.1password.com): my.1password.com            
Enter the email address for your account on my.1password.com: <ACCOUNT_EMAIL>
Enter the Secret Key for [email protected] on my.1password.com: <SECRET_KEY>
Enter the password for [email protected] at my.1password.com: 
Enter your six-digit authentication code: <OTP_CODE>
Now run 'eval $(op signin)' to sign in.

Перевіряємо акаунти:

root@setevoy-nas:/home/setevoy # op account list
SHORTHAND    URL                         EMAIL            USER ID
my           https://my.1password.com    [email protected]     7BS***KMM

Логінимось:

root@setevoy-nas:/home/setevoy # eval $(op signin)
Enter the password for [email protected] at my.1password.com:

І маємо доступ до сікретів.

Наприклад, отримати ключ, який додамо далі:

root@setevoy-nas:/home/setevoy # op item get "FreeBSD NAS SSH"
ID:          ulz***4ce
Title:       FreeBSD NAS SSH
Vault:       Personal (wb7***guq)
Created:     1 week ago
Updated:     1 week ago by Arseny
Favorite:    false
Tags:        FreeBSD,SSH
Version:     1
Category:    LOGIN
Fields:
  password:    [use 'op item get ulz***4ce --reveal' to reveal]
  username:    setevoy

Linux, 1Password та SSH Agent

Колись більш детально писав в пості SSH: RSA-ключи и ssh-agent – управление SSH-ключами и их паролями (2019 рік), але так як зараз активно користуюсь 1Password, який вміє як зберігати самі ключі – так і виступати в ролі SSH Agent.

Документація – 1Password SSH agent та Get started with 1Password for SSH.

Отримуємо приватний ключ:

[setevoy@setevoy-work ~] $ cat .ssh/freebsd-nas
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
...
vKI8A2GTAGTqppS0yVz2AAAAD3NldGV2b3lAc2V0ZXZveQECAwQFBg==
-----END OPENSSH PRIVATE KEY-----

Копіюємо, і додаємо новий ключ в 1Password (хоча він може генерувати ключі сам):

Переходимо в Settings:

Переходимо в Developer – Set up the SSH Agent:

1Password покаже зміст файлу ~/.ssh/config і навіть запропонує оновити його:

Але в мене зараз в конфігу вже є трохи налаштувань:

# GitHub.com
Host github.com
  PreferredAuthentications publickey
  IdentityFile /home/setevoy/.ssh/setevoy_main_priv_openssh

ExitOnForwardFailure yes

Тому додаємо в кінець файлу вручну.

Вказуємо два хости – nas.setevoy та, на випадок, якщо Unbound недоступний, то IP-адресу хоста з FreeBSD:

...

Host nas.setevoy 192.168.0.2
  IdentityAgent ~/.1password/agent.sock

Виконуємо ssh nas.setevoy, 1Password запросить підтвердження – пароль самого 1Password:

І тепер все працює через його SSH Agent:

[setevoy@setevoy-work ~] $ ssh nas.setevoy
...
Last login: Sat Dec 27 09:03:15 2025 from 192.168.0.4
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!

...
[setevoy@setevoy-nas ~]$ 

Як працює SSH Agent

При підключені SSH-клієнт:

  • читає конфігурацію (~/.ssh/config, опції CLI)
  • підключається до SSH-агента (у нашому випадку 1Password) і отримує список доступних публічних ключів
  • по черзі пропонує серверу публічні ключі, які в нього є (з агента і, за відсутності обмежень, з ~/.ssh/)
  • sshd на сервері дивиться в ~/.ssh/authorized_keys конкретного юзера і перевіряє, чи є там запропонований публічний ключ
  • коли сервер знаходить збіг, він приймає цей публічний ключ і надсилає клієнту дані для підпису (випадковий набір чисел)
  • клієнт передає ці дані SSH-агенту, який володіє відповідним приватним ключем
  • агент створює криптографічний підпис приватним ключем і повертає його клієнту
  • сервер перевіряє підпис публічним ключем і завершує аутентифікацію

Глянути це можемо з ssh -v user@host:

[setevoy@setevoy-work ~] $ ssh -v [email protected]
debug1: OpenSSH_10.2p1, OpenSSL 3.6.0 1 Oct 2025
debug1: Reading configuration data /home/setevoy/.ssh/config
...
debug1: Connecting to nas.setevoy [192.168.0.2] port 22.
debug1: Connection established.
...
debug1: Authenticating to nas.setevoy:22 as 'setevoy'
...
debug1: Will attempt key: RTFM RSA SHA256:nedb3Qgpkxgu57MRP7/eXShHgw6N6b7SjZ3S1rNyFb4 agent
debug1: Will attempt key: FreeBSD-NAS ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA agent
debug1: Will attempt key: /home/setevoy/.ssh/id_rsa 
debug1: Will attempt key: /home/setevoy/.ssh/id_ecdsa 
debug1: Will attempt key: /home/setevoy/.ssh/id_ecdsa_sk 
debug1: Will attempt key: /home/setevoy/.ssh/id_ed25519 ED25519 SHA256:X8L1lCBQz8Bk7K5rMGqiE+tlSthCbgaqK7ryLZ6gVWU
debug1: Will attempt key: /home/setevoy/.ssh/id_ed25519_sk 
debug1: Offering public key: RTFM RSA SHA256:nedb3Qgpkxgu57MRP7/eXShHgw6N6b7SjZ3S1rNyFb4 agent
debug1: Authentications that can continue: publickey
debug1: Offering public key: FreeBSD-NAS ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA agent
debug1: Server accepts key: FreeBSD-NAS ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA agent
Authenticated to nas.setevoy ([192.168.0.2]:22) using "publickey".
...
Last login: Sun Dec 28 13:44:51 2025 from 192.168.0.4
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!
...

[setevoy@setevoy-nas ~]$

І для ключів “Will attempt key: RTFM RSA [...] та FreeBSD-NAS [...] як раз бачимо, що вони були отримані від agent.

SSH Agent та помилка “Too many authentication failures”

Вище бачили, що передається ціла пачка ключів, і якщо їх багато – то сервер може відкинути подальшу аутентифікацію.

Кількість спроб задається на сервері параметром MaxAuthTries (дефолт “6”) в /etc/ssh/sshd_config. Тобто, якщо маємо 7 ключів, і перші 6 не підійшли – то підключитись не зможемо.

Аби вказати SSH клієнту який конкретно приватний ключ з агента використовувати – вказуємо публічний ключ та задаємо IdentitiesOnly:

...

Host nas.setevoy 192.168.0.2
  IdentityAgent ~/.1password/agent.sock
  IdentityFile ~/.ssh/freebsd-nas.pub
  IdentitiesOnly yes

І тепер під час підключення клієнт буде передавати тільки цей ключі:

[setevoy@setevoy-work ~] $ ssh -v [email protected]
...
debug1: get_agent_identities: agent returned 2 keys
debug1: Will attempt key: /home/setevoy/.ssh/freebsd-nas.pub ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA explicit agent
debug1: Offering public key: /home/setevoy/.ssh/freebsd-nas.pub ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA explicit agent
debug1: Server accepts key: /home/setevoy/.ssh/freebsd-nas.pub ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA explicit agent
Authenticated to nas.setevoy ([192.168.0.2]:22) using "publickey".
...

Last login: Sun Dec 28 13:44:57 2025 from 192.168.0.4
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!
...

[setevoy@setevoy-nas ~]$

Basic SSH Server Hardening

Див. sshd_config.

Перевірити поточну конфігурацію можна з sshd -T:

root@setevoy-nas:/home/setevoy # sshd -T 
port 22
addressfamily any
listenaddress 0.0.0.0:22
...

Наприклад, чи дозволений логін root:

root@setevoy-nas:/home/setevoy # sshd -T | grep root
permitrootlogin no

Редагуємо конфіг /etc/ssh/sshd_config, задаємо мінімальні параметри, і нехай буде PermitRootLogin no вказаний тут явно.

Плюс явно вказуємо дозвіл на PubkeyAuthentication та PasswordAuthentication – парольну аутентифікацію відключимо пізніше, коли впевнимось, що все працює:

PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication yes

Перевіряємо синтаксис з sshd -t, якщо все добре – то команда просто нічого не виведе:

root@setevoy-nas:/home/setevoy # sshd -t; echo $?
0

Виконуємо reload конфігу:

root@setevoy-nas:/home/setevoy # service sshd reload
Performing sanity check on sshd configuration.

Перевіряємо чи працює доступ з Linux:

[setevoy@setevoy-work ~]  $ ssh nas.setevoy
...
[setevoy@setevoy-nas ~]$

pf вже налаштований, див. FreeBSD: Home NAS, part 2 – знайомство з Packet Filter (PF) firewall:

...

# allow SSH from Office LAN (192.168.0.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.0.0/24 to (em0) port 22 keep state

# allow SSH from Home network (192.168.100.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.100.0/24 to (em0) port 22 keep state

# allow SSH from VPN clients to FreeBSD host
pass in on wg0 proto tcp from 10.8.0.0/24 to (wg0) port 22 keep state
...

Можна мережі вказати як { 192.168.0.0/24, 192.168.100.0/24, 10.8.0.0/24 }, але для SSH вирішив залишити в такому вигляді, аби було явно видно.

Далі можна додати ще трохи параметрів:

  • PermitRootLogin no: заборона підключення root, це вже є
  • PubkeyAuthentication yes: аутентифікація по ключам, теж вже є
  • PasswordAuthentication yes: пароль аутентифікація, поки залишаємо включеною
  • ChallengeResponseAuthentication no: вимикаємо keyboard-interactive аутентифікацію через PAM, яка не потрібна без 2FA
    • тут ще нюанс в тому, що навіть якщо відключити PasswordAuthentication, але ChallengeResponseAuthentication буде “yes” і при UsePAM = “yes” – то система все одно може запросити парольну аутентифікацію
  • UsePAM yes: залишаємо включеним для логування, обліку сесій, політик доступу (див. Practical Effects of Setting “UsePAM yes” on SSH in Linux)
  • AllowUsers setevoy або AllowGroups wheel: яким юзерам або групам можна підключатись по SSH
  • X11Forwarding no: блокуємо форвард X11 – графічного серверу тут все одно нема
  • AllowAgentForwarding no: забороняємо можливість використання локальних SSH-ключів клієнта з сервера через SSH-агент клієнта
  • AllowTcpForwarding no: забороняємо SSH-тунелі – на домашньому NAS це точно не треба (див. SSH-туннели в примерах – мій пост за 2013 рік, OMG… та SSH Tunnels: Secure Remote Access and Port Forwarding)
  • AuthorizedKeysFile .ssh/authorized_keys: це дефолтне значення, але задамо явно

Перевіряємо ще раз з sshd -t, виконуємо reload, перевіряємо підключення з ключем з клієнта.

Якщо все ОК – то додаємо ще трохи тюнингу:

  • PasswordAuthentication no: тепер відключаємо парольну аутентифікацію
  • AuthenticationMethods publickey: явно заборонити всі механізми окрім ключів
  • MaxSessions 2: максимальна кількість активних сесій на одне TCP-підключення
  • LoginGraceTime 30: кількість секунд, за яку клієнт має пройти аутентифікацію

Для ще більшої безпеки – можна налаштувати інший порт замість 22 (наприклад, задати Port 2222), обмежити IP, на яких слухає sshd (параметр ListenAddress 192.168.0.2), і навіть налаштувати Two-factor Authentication For SSH – але для домашнього сервера з доступом тільки з локальних мереж це вже overkill.

Loading

FreeBSD: Home NAS, part 6 – Samba server та підключення клієнтів
0 (0)

27 Грудня 2025

Продовжуємо налаштовувати домашній NAS на FreeBSD.

Власне, NAS – Network System, і хочеться мати до нього доступ з інших девайсів – з Linux та Windows хостів, з телефонів, телевізорів.

Тут у нас на вибір дві основні опції – Samba та NFS. Можна, звісно, згадати і sshfs – але це рішення точно не для домашньої мережі (хоча простіше).

Я для себе вирішив для Windows (на ігровому ПК), Android телефонів та Android TV зробити доступ з Samba share – а NFS буде чисто для Linux систем для бекапів.

В цьому пості налаштуємо Samba на FreeBSD, а в наступному – додамо NFS.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

UPD: випадково нагуглив свій власний пост за 2012 рік – FreeBSD: установка и быстрая настройка сервера SAMBA… Зовсім забув, що колись вже це робив.

Установка Samba

Для Samba share створимо окремий ZFS dataset:

root@setevoy-nas:/home/setevoy # zfs create nas/share

Перевіряємо його:

root@setevoy-nas:/home/setevoy # zfs list nas/share
NAME        USED  AVAIL  REFER  MOUNTPOINT
nas/share    96K  3.51T    96K  /nas/share

Права доступу зараз – root:wheel:

root@setevoy-nas:/home/setevoy # ls -ld /nas/share
drwxr-xr-x  2 root wheel 2 Dec 26 15:46 /nas/share

Додаємо в систему нову групу:

root@setevoy-nas:/home/setevoy # pw groupadd smbshare

Додаємо нового юзера:

root@setevoy-nas:/home/setevoy # pw useradd smbshare -s /usr/sbin/nologin -g smbshare

Тут:

  • без домашньої директорії
  • без можливості логіна в систему – /usr/sbin/nologin
    • але якщо планується виконувати локальні операції від юзера – то створюємо з -s /bin/sh, а SSH все одно буде заблокований в sshd config (про SSH буде окремим постом)
  • і з -g smbshare вказуємо primary group

Перевіряємо юзера:

root@setevoy-nas:/home/setevoy # id smbshare
uid=1004(smbshare) gid=1004(smbshare) groups=1004(smbshare)

Міняємо права доступу на директорію для Samba:

root@setevoy-nas:/home/setevoy # chown smbshare:smbshare /nas/share

І права доступу до каталога тепер – можна глянути розширену інформацію з getfacl, аби отримати POSIX/NFSv4 ACL, які ZFS використовує замість класичних Unix-бітів:

root@setevoy-nas:/home/setevoy # getfacl /nas/share
# file: /nas/share
# owner: smbshare
# group: smbshare
            owner@:rwxp--aARWcCos:-------:allow
            group@:rwxp--a-R-c--s:-------:allow
         everyone@:------a-R-c--s:-------:allow

Тут для owner заданий повний доступ:

  • r: read data / list dir
  • w: write data / create file
  • x: execute / traverse
  • p: append
  • a: write ACL
  • A: read ACL
  • R: read attributes
  • W: write attributes
  • c: read named attributes
  • C: write named attributes
  • o: read ownership
  • s: write ownership

Налаштування ZFS ACL

Samba буде встановлювати власні права доступу, тому в параметрах ZFS ACL треба переключити режими.

Samba керує правами доступу самостійно, тому для ZFS ACL потрібно увімкнути режими passthrough, щоб ZFS не змінював ACL.

Перевіряємо параметри датасету зараз:

root@setevoy-nas:/home/setevoy # zfs get acltype,aclmode,aclinherit nas/share
NAME       PROPERTY    VALUE          SOURCE
nas/share  acltype     nfsv4          default
nas/share  aclmode     discard        default
nas/share  aclinherit  restricted     default

Задаємо aclmode та aclinherit в значення passthrough – тоді ZFS буде використовувати ті права, які задає Samba:

root@setevoy-nas:/home/setevoy # zfs set aclmode=passthrough nas/share
root@setevoy-nas:/home/setevoy # zfs set aclinherit=passthrough nas/share

Перевіряємо тепер:

root@setevoy-nas:/home/setevoy # zfs get acltype,aclmode,aclinherit nas/share
NAME       PROPERTY    VALUE          SOURCE
nas/share  acltype     nfsv4          default
nas/share  aclmode     passthrough    local
nas/share  aclinherit  passthrough    local

На відміну від acltype=off, який повністю вимикає ACL і залишає лише POSIX-права, режим passthrough дозволяє Samba керувати ACL без втручання ZFS.

Надалі зміни в директорії /nas/share краще виконувати від імені smbshare або root, а не звичайного користувача, щоб не порушувати модель прав доступу Samba.

Samba – налаштування загального доступу

Встановлюємо стабільну для FreeBSD версію 4.16:

root@setevoy-nas:~ # pkg install samba416

Додаємо до /etc/rc.conf:

root@setevoy-nas:~ # sysrc samba_server_enable=YES 
samba_server_enable: -> YES

Створюємо файл /usr/local/etc/smb4.conf з мінімальним конфігом:

[global]
   workgroup = WORKGROUP
   security = user

[shared]
   path = /nas/share
   read only = no

Тут:

  • workgroup: задаємо дефолтне WORKGROUP, тоді не треба буде вказувати на клієнтах
  • security = user: аутентифікацію та перевірку прав доступів виконує сама Samba
  • [shared]: ім’я шари (share name), яке потім будемо використовувати для підключень
  • path = /nas/share: реальний шлях на сервері

Див. smb4.conf.

Або більше детально, просто явно вказуємо деякі дефолтні опції:

[global]
   workgroup = WORKGROUP
   security = user

[shared]
   path = /nas/share
   read only = no
   browseable = yes
   valid users = @smbshare
   create mask = 0660
   directory mask = 2770

Перевіряємо синтаксис:

root@setevoy-nas:/home/setevoy # testparm
Load smb config files from /usr/local/etc/smb4.conf
Loaded services file OK.
Weak crypto is allowed

Server role: ROLE_STANDALONE

Press enter to see a dump of your service definitions

# Global parameters
[global]
        security = USER
        idmap config * : backend = tdb


[shared]
        create mask = 0660
        directory mask = 02770
        path = /nas/share
        read only = No
        valid users = @smbshare

Додаємо Samba-юзера:

root@setevoy-nas:/home/setevoy # smbpasswd -a smbshare
New SMB password:
Retype new SMB password:
Added user smbshare.

smbpasswd збереже його пароль в /var/db/samba4/private/passdb.tdb.

Запускаємо сервіс:

root@setevoy-nas:~ # service samba_server start 
Performing sanity check on Samba configuration: OK 
Starting nmbd. 
Starting smbd.

Перевіряємо порт:

root@setevoy-nas:/home/setevoy # sockstat -4 | grep smb
root     smbd       17621 31  tcp4   *:445                 *:*
root     smbd       17621 32  tcp4   *:139                 *:*

Пробуємо підключитись локально:

root@setevoy-nas:/home/setevoy # smbclient //localhost/shared -U smbshare
Password for [WORKGROUP\smbshare]:
Try "help" to get a list of possible commands.
smb: \>

Робимо list files – поки тут пусто:

smb: \> ls
  .                                   D        0  Fri Dec 26 15:46:55 2025
  ..                                  D        0  Fri Dec 26 15:46:55 2025

                3771191492 blocks of size 1024. 3771191396 blocks available

Копіюємо щось із системи:

smb: \> put /etc/hosts hosts.test
putting file /etc/hosts as \hosts.test (252.7 kb/s) (average 252.7 kb/s)
smb: \> ls hosts.test
  hosts.test                          A     1035  Fri Dec 26 16:14:29 2025

                3771191480 blocks of size 1024. 3771191380 blocks available

І тепер маємо файл в /nas/share:

root@setevoy-nas:/home/setevoy # ls -l /nas/share
total 5
-rw-rw----  1 smbshare smbshare 1035 Dec 26 16:14 hosts.test

Відкриваємо доступ до Samba на pf firewall (див. FreeBSD: Home NAS, part 2 – знайомство з Packet Filter (PF) firewall):

...
### SMB
pass in on em0 proto tcp from { 192.168.0.0/24, 192.168.100.0/24, 10.8.0.0/24 } to any port 445 keep state
...

Перевіряємо конфіг pf, виконуємо reload:

root@setevoy-nas:/home/setevoy # pfctl -vnf /etc/pf.conf && service pf reload

Arch Linux та Samba share

Встановлюємо пакети:

[setevoy@setevoy-work ~] $ sudo pacman -S smbclient cifs-utils
  • smbclient: Samba CLI
  • cifs-utils: утиліти для роботи з Samba share через CIFS:
    • mount -t cifs
    • запису в /etc/fstab
    • systemd automount

Перевіряємо підключення з Linux до Samba на FreeBSD:

[setevoy@setevoy-work ~]  $ smbclient //192.168.0.2/shared -U smbshare
Can't load /etc/samba/smb.conf - run testparm to debug it
Password for [WORKGROUP\smbshare]:
Try "help" to get a list of possible commands.
smb: \>

Ще раз list – все на місці:

smb: \> ls hosts.test
  hosts.test                          A     1035  Fri Dec 26 16:14:29 2025

                3771191480 blocks of size 1024. 3771191380 blocks available

Налаштування /etc/fstab та systemd-automount

Якщо хочемо, аби шара підключалась автоматично – налаштуємо /etc/fstab та systemd-automount.

На клієнті створюємо каталог, в який буде підключатись /nas/share з серверу:

[setevoy@setevoy-work ~] $ sudo mkdir -p /mnt/nas-shared

Створюємо файл /root/.smbcredentials з логіном та паролем:

username=smbshare
password=<PASSWORD>

Задаємо доступ тільки для root:

[setevoy@setevoy-work ~] $ sudo chmod 600 /root/.smbcredentials

Отримуємо user id:

[setevoy@setevoy-work ~]  $ id setevoy
uid=1000(setevoy) gid=1000(setevoy) groups=1000(setevoy),998(wheel),964(ollama),962(docker)

Редагуємо /etc/fstab:

...
# NAS Samba share
//192.168.0.2/shared  /mnt/nas-shared  cifs  credentials=/root/.smbcredentials,iocharset=utf8,uid=1000,gid=1000,nofail,_netdev,noauto,x-systemd.automount,serverino,noperm  0  0

Тут:

  • 192.168.0.2/shared: адреса хоста та ім’я шари з /usr/local/etc/smb4.conf
  • /mnt/nas-shared: куди монтуємо локально
  • cifs: ти файлової системи
  • credentials=/root/.smbcredentials: де брати логін та пароль
  • iocharset=utf8: використовувати UTF-8 для імен файлів (коректні кирилиця та спецсимволи).
  • uid=1000: файли на клієнті відображаються як власність користувача з UID 1000 (робили id setevoy вище)
  • gid=1000: файли відображаються з групою GID 1000
  • nofail: система завантажиться навіть якщо ресурс недоступний (must have для мережевих систем)
  • _netdev: мережевий ресурс, монтується після підняття мережі
  • noauto: не монтувати автоматично під час boot
  • x-systemd.automount: монтувати автоматично при першому доступі через systemd-unit (див. далі)
  • serverino: використовувати inode-номери сервера
  • noperm: ігнорувати локальну перевірку прав доступу, покладаючись на сервер (Samba)

Зберігаємо зміни, виконуємо sudo systemctl daemon-reload, і тепер у нас є два нових файли в /run/systemd/generator/:

[setevoy@setevoy-work ~]  $ ll /run/systemd/generator/ | grep share
-rw-r--r-- 1 root root 176 Dec 27 07:41 mnt-nas\x2dshared.automount
-rw-r--r-- 1 root root 342 Dec 27 07:41 mnt-nas\x2dshared.mount

Через файл mnt-nas\x2dshared.automount systemd відстежує доступ до /mnt/nas-shared, і як тільки ми виконаємо якусь дію (наприклад, cd /mnt/nas-shared) – systemd виконає mnt-nas\x2dshared.mount, який власне підключить розділ.

Див. systemd.automount та Understanding systemd Automounts: Why /dev/sda1 Becomes systemd-1 and How to Tame It.

Активуємо їх:

[setevoy@setevoy-work ~] $ sudo systemctl restart remote-fs.target

Перевіряємо статус – має бути active (waiting):

[setevoy@setevoy-work ~] $ sudo systemctl status mnt-nas\x2dshared.automoun
● nas-smb-media.automount
     Loaded: loaded (/etc/fstab; generated)
     Active: active (waiting) since Mon 2025-12-29 19:32:28 EET; 2s ago
 Invocation: b50ba77b54b941409314fc33068de0d1
   Triggers: ● nas-smb-media.mount
      Where: /nas/smb/media
       Docs: man:fstab(5)
             man:systemd-fstab-generator(8)

Перевіряємо – виконуємо якусь операцію над /mnt/nas-shared, і systemd з секундною затримкою підмонтує Samba share:

[setevoy@setevoy-work ~]  $ ls -l /mnt/nas-shared/
total 10626126
-rwxr-xr-x 1 setevoy setevoy 1359900672 Dec 26 17:03  FreeBSD-15.0-RELEASE-amd64-disc1.iso
-rwxr-xr-x 1 setevoy setevoy 9817684824 Dec 26 14:35 'Odin.doma.(1990).BDRip.1080p.[envy].[60fps].mkv'
-rwxr-xr-x 1 setevoy setevoy       1035 Dec 26 16:14  hosts.test

І тепер каталог є в mounted:

[setevoy@setevoy-work ~]  $ findmnt /mnt/nas-shared
TARGET          SOURCE FSTYPE OPTIONS
/mnt/nas-shared systemd-1
                       autofs rw,relatime,fd=94,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=17332707
/mnt/nas-shared //192.168.0.2/shared
                       cifs   rw,relatime,vers=3.1.1,cache=strict,upcall_target=app,username=smbshare,uid=1000,forceuid,gid=1000,forcegid,addr=192.168.0.2,file_mode=0755,dir_mode=0755,iocharset=utf

Перевірка швидкості запису

Робив, аби порівняти з NFS – різниці не побачив, але нехай тут буде приклад.

Самий надійний варіант – копіювати з dd, додавши oflag=direct та status=progress:

$ dd if=Downloads/FreeBSD-15.0-RELEASE-amd64-disc1.iso of=/mnt/nas-shared/FreeBSD-15.0-RELEASE-amd64-disc1.iso bs=16M oflag=direct status=progress
1325400064 bytes (1.3 GB, 1.2 GiB) copied, 23 s, 57.5 MB/s1359900672 bytes (1.4 GB, 1.3 GiB) copied, 23.5906 s, 57.6 MB/s

81+1 records in
81+1 records out
1359900672 bytes (1.4 GB, 1.3 GiB) copied, 23.7558 s, 57.2 MB/s

57.2 MB/s, тобто 457.6 Mbit/s. При тому, що ноут зараз на WiFi – цілком нормальна швидкість.

Підключення Samba клієнтів

І приклади того, як до Samba підключитись з різних девайсів.

Samba та KDE Dolphin

Взагалі, ми вже зробили automount через systemd, але можна відкрити напряму з KDE Dolphin file manager – вказуємо адресу smb://192.168.0.2, вводимо логін-пароль:

І маємо доступ до файлів на сервері:

Samba та Android phone

Спробував Solid Explorer File Manager – простий, все працює.

Додаємо в ньому нове підключення LAN/SMB:

Вказуємо адресу та порт сервера:

Вибираємо аутентифікацію по логіну-паролю:

Вказуємо їх:

Підтверджуємо:

І підключаємось:

Файли на сервері тепер доступні з телефону:

Samba та Android TV

Доступ з File Manager

Для Android TV є додаток File Manager, встановлюємо:

Вибираємо новий location, тип Remote:

Вибираємо тип SMB:

Вказуємо тільки адресу, без порта, логін та пароль:

І тепер файли доступі з телевізора:

Відео-файли можна запускати прямо звідси – або налаштувати MX Player.

Доступ з MX Player

Аналогічно – встановлюємо додаток, відкриваємо налатування, вибираємо Local network:

Вказуємо IP, логін, пароль:

І маємо доступ до відеофайлів:

Готово.

Loading

FreeBSD: Home NAS, part 5 – ZFS pool, datasets, snapshots та моніторинг
0 (0)

22 Грудня 2025

Продовжую for fun and profit сетапити собі домашній сервер з FreeBSD на Lenovo ThinkCentre M720s SFF.

І сьогодні, нарешті, зробимо основне – налаштуємо ZFS pool на реальних дисках, подивимось на роботу з datasets, шифруванням, снапшотами, моніторингом.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Підготовка дисків

Дивимось, які диски фізично присутні в системі:

root@setevoy-nas:~ # geom disk list
Geom name: nda0
Providers:
1. Name: nda0
   Mediasize: 500107862016 (466G)
   ...

Geom name: ada0
Providers:
1. Name: ada0
   Mediasize: 4000787030016 (3.6T)
   ...

Geom name: ada1
Providers:
1. Name: ada1
   Mediasize: 4000787030016 (3.6T)
   ...

Або з camcontrol:

root@setevoy-nas:~ # camcontrol devlist
<Samsung SSD 870 EVO 4TB SVT03B6Q>  at scbus0 target 0 lun 0 (pass0,ada0)
<Samsung SSD 870 EVO 4TB SVT03B6Q>  at scbus1 target 0 lun 0 (pass1,ada1)
<CT500P310SSD8 VACR001>            at scbus7 target 0 lun 1 (pass3,nda0)

Тут в мене ada1 та ada2 – це SATA-диски для самого NAS storage, а nda0 – NVMe, на якому встановлена система (там зараз UFS, але потім, скоріш за все, перевстановлю із ZFS теж).

Імена пристроїв на кшталт /dev/ada0, /dev/ada1 або /dev/nda0 можуть змінюватися залежно від порядку підключення дисків, а тому напряму використовувати їх у ZFS не рекомендується – далі створимо власні GPT labels.

Створення GPT tables

Про всяк випадок – видаляємо існуючі таблиці розділів.

ВАЖЛИВО: таблиця розділів буде знищена. Дані залишаються на носії, але стануть недоступними.

В моєму випадку – диски нові, тому бачимо помилку “Invalid argument“:

root@setevoy-nas:~ # gpart destroy -F ada0
gpart: arg0 'ada0': Invalid argument
root@setevoy-nas:~ # gpart destroy -F ada1
gpart: arg0 'ada1': Invalid argument

Створюємо таблиці GPT (GUID Partition Table):

root@setevoy-nas:~ # gpart create -s gpt ada0
ada0 created
root@setevoy-nas:~ # gpart create -s gpt ada1
ada1 created

Перевіряємо з gpart:

root@setevoy-nas:~ # gpart show ada0
=>        40  7814037088  ada0  GPT  (3.6T)
          40  7814037088        - free -  (3.6T)

root@setevoy-nas:~ # gpart show ada1
=>        40  7814037088  ada1  GPT  (3.6T)
          40  7814037088        - free -  (3.6T)

Створення GPT labels

Для подальшої роботи з дисками створимо постійні GPT lables – іменовані ідентифікатори розділів, які зберігаються в GPT-таблиці розділів і читаються при старті системи.

Вони не змінюються після reboot, не залежать від порядку SATA портів, не залежать від того, як ядро виявило диск.

Додаємо для обох дисків:

root@setevoy-nas:~ # gpart add -t freebsd-zfs -l zfs_disk1 ada0
ada0p1 added
root@setevoy-nas:~ # gpart add -t freebsd-zfs -l zfs_disk2 ada1
ada1p1 added

Перевіряємо в /dev/gpt/:

root@setevoy-nas:~ # ls -l /dev/gpt/
total 0
crw-r-----  1 root operator 0x9b Dec 19 13:32 zfs_disk1
crw-r-----  1 root operator 0xa9 Dec 19 13:32 zfs_disk2

Або з gpart:

root@setevoy-nas:/home/setevoy # gpart show -l ada0
=>        40  7814037088  ada0  GPT  (3.6T)
          40  7814037088     1  zfs_disk1  (3.6T)

root@setevoy-nas:/home/setevoy # gpart show -l ada1
=>        40  7814037088  ada1  GPT  (3.6T)
          40  7814037088     1  zfs_disk2  (3.6T)

Або з glabel:

root@setevoy-nas:~ # glabel status
                                      Name  Status  Components
                             gpt/zfs_disk1     N/A  ada0p1
gptid/67ebfac9-dcce-11f0-98bf-00d861f3bff0     N/A  ada0p1
               diskid/DISK-S758NX0Y701757D     N/A  ada0
                             gpt/zfs_disk2     N/A  ada1p1
gptid/6a9c3ee5-dcce-11f0-98bf-00d861f3bff0     N/A  ada1p1
               diskid/DISK-S758NX0Y701756A     N/A  ada1

Диски готові – переходимо до ZFS, нам треба:

  • налаштувати ZFS pool з mirror
  • створити datasets
  • подивитись на шифрування даних
  • перевірити, як працювати зі snapshots

І в кінці окремо поговоримо про моніторинг дисків та ZFS pool.

Створення ZFS mirror pool

Використовуємо такі параметри:

  • ashift=12: розмір фізичного сектора, який ZFS використовує для I/O
    • розмір сектора визначається як 2^ashift байт, тобто  2¹² = 4096 байт
    • замінити значення ashift після створення Pool не можна
  • atime=off: вимикаємо оновлення часу доступу до файлів, зменшує кількість зайвих записів на диск
  • compression=lz4: швидка компресія даних на диску з мінімальним CPU-оверхедом
  • xattr: налаштування зберігання атрибутів файлі:
    • xattr=on: старий і default варіант, extended attributes зберігаються як окремі приховані файли
    • xattr=sa: xattr зберігаються безпосередньо в dnode файлу (аналог inode в UFS/ext4), без створення окремих прихованих файлів – менше звернень до диска і краща продуктивність
  • mirror: використовуємо ZFS mirror (аналог RAID1) – дані синхронно записуються на обидва диски (див. також vdev)

Див. ZFS Tuning Recommendations.

Створюємо пул з дисків /dev/gpt/zfs_disk1 та /dev/gpt/zfs_disk2:

root@setevoy-nas:~ # zpool create -o ashift=12 -O atime=off -O compression=lz4 -O xattr=sa nas mirror /dev/gpt/zfs_disk1 /dev/gpt/zfs_disk2

Перевіряємо:

root@setevoy-nas:~ # zpool status
  pool: nas
 state: ONLINE
config:

        NAME               STATE     READ WRITE CKSUM
        nas                ONLINE       0     0     0
          mirror-0         ONLINE       0     0     0
            gpt/zfs_disk1  ONLINE       0     0     0
            gpt/zfs_disk2  ONLINE       0     0     0

errors: No known data errors

Додаємо підключення при ребутах:

root@setevoy-nas:/home/setevoy # sysrc zfs_enable=YES
zfs_enable: NO -> YES

Створення ZFS datasets

ZFS dataset – це окрема файлова система всередині ZFS pool, яка має власні властивості (compression, quota, mountpoint тощо) і керується незалежно від інших datasets.

Зараз у нас один, корневий dataset:

root@setevoy-nas:~ # zfs list
NAME   USED  AVAIL  REFER  MOUNTPOINT
nas    420K  3.51T    96K  /nas

Корінь pool – це технічний корінь, а не місце для даних, тому створимо кілька окремих.

Не факт, що в мене датасети залишаться такими і надалі, але просто як ідея того, як можна розділити простір на дисках:

  • nas/data: основний dataset для збереження всяких даних типу музики-фільмів
  • nas/backups: сюди можна буде копіювати якісь periodic бекапи з робочого і домашнього ноутбуків
  • nas/private: зашифрований розділ для приватних даних та/або баз даних типу KeePass або бекапів 1Password
  • nas/shared: загальнодоступний dataset для доступу з телефонів і ноутбуків через Samba share (про налаштування Samba – окремим постом, вже є в чернетках)

Створюємо новий датасет з іменем nas/data:

root@setevoy-nas:~ # zfs create nas/data

Перевіряємо список ще раз:

root@setevoy-nas:~ # zfs list
NAME       USED  AVAIL  REFER  MOUNTPOINT
nas        540K  3.51T    96K  /nas
nas/data    96K  3.51T    96K  /nas/data

І каталог цього датасету:

root@setevoy-nas:~ # ls -la /nas/data/
total 1
drwxr-xr-x  2 root wheel 2 Dec 19 13:41 .
drwxr-xr-x  3 root wheel 3 Dec 19 13:41 ..

Він жеж має власну точку підключення:

root@setevoy-nas:/home/setevoy # mount | grep data
nas/data on /nas/data (zfs, local, noatime, nfsv4acls)

Яка задана у властивостях датасету:

root@setevoy-nas:/home/setevoy # zfs get mountpoint nas/data
NAME      PROPERTY    VALUE       SOURCE
nas/data  mountpoint  /nas/data   default

Додамо ще один:

root@setevoy-nas:~ # zfs create nas/backups

Ще раз список:

root@setevoy-nas:~ # zfs list
NAME          USED  AVAIL  REFER  MOUNTPOINT
nas           696K  3.51T    96K  /nas
nas/backups    96K  3.51T    96K  /nas/backups
nas/data       96K  3.51T    96K  /nas/data

Шифрування dataset

Хочеться мати окремий dataset для чутливих даних, тому глянемо, як це зроблено у ZFS.

Документація – Encrypting ZFS File Systems.

Важливо:

  • включити шифрування можна тільки створенні dataset – але можна відключити після
  • якщо батьківський dataset не зашифрований – дочірній можна зашифрувати
  • якщо батьківський зашифрований – дочірні успадковують його шифрування

Створимо новий dataset, вказуємо, що шифрується з паролем:

root@setevoy-nas:~ # zfs create -o encryption=on -o keyformat=passphrase -o keylocation=prompt nas/private
Enter new passphrase:
Re-enter new passphrase:

Перевіряємо цей датасет:

root@setevoy-nas:~ # zfs get encryption,keyformat,keylocation nas/private
NAME         PROPERTY     VALUE        SOURCE
nas/private  encryption   aes-256-gcm  -
nas/private  keyformat    passphrase   -
nas/private  keylocation  prompt       local

Як це буде виглядати після reboot:

  • pool nas імпортується
  • nas/private буде заблокований, і mountpoint не зʼявиться, доки вручну не ввести пароль і не розблокувати його

Для розблокування потім використовуємо:

root@setevoy-nas:~ # zfs load-key nas/private
root@setevoy-nas:~ # zfs mount nas/private

На відміну від, наприклад, шифрування розділів з LUKS – ZFS дозволяє змінювати пароль для зашифрованого dataset без перешифрування даних. Фактично змінюється лише ключ, який захищає основний ключ шифрування.

Якщо хочемо змінити пароль – виконуємо zfs change-key:

root@setevoy-nas:~ # zfs change-key nas/private
Enter new passphrase for 'nas/private':

Замість використання паролю можемо створити файл-ключ, який буде використаний про ребутах для підключення датасету.

Генеруємо ключ:

root@setevoy-nas:/home/setevoy # dd if=/dev/random of=/root/nas-private-pass.key bs=32 count=1

Змінюємо пароль, який вказали при створені датасету:

root@setevoy-nas:/home/setevoy # zfs change-key -o keyformat=raw -o keylocation=file:///root/nas-private-pass.key nas/private

Перевіряємо атрибути:

root@setevoy-nas:/home/setevoy # zfs get encryption,keyformat,keylocation nas/private
NAME         PROPERTY     VALUE                              SOURCE
nas/private  encryption   aes-256-gcm                        -
nas/private  keyformat    raw                                -
nas/private  keylocation  file:///root/nas-private-pass.key  local

Ребутаємо машину:

root@setevoy-nas:/home/setevoy # shutdown -r now
Shutdown NOW!
shutdown: [pid 13519]

І потім перевіряємо розділи:

root@setevoy-nas:/home/setevoy # mount | grep nas
nas on /nas (zfs, local, noatime, nfsv4acls)
nas/backups on /nas/backups (zfs, local, noatime, nfsv4acls)
nas/data on /nas/data (zfs, local, noatime, nfsv4acls)

Та з zfs list:

root@setevoy-nas:/home/setevoy # zfs list
NAME                        USED  AVAIL  REFER  MOUNTPOINT
nas                         200G  3.32T   112K  /nas
nas/backups                 200K   500G   104K  /nas/backups
nas/data                     96K  3.51T    96K  /nas/data
nas/private                 200K  3.32T   200K  /nas/private

Налаштування dataset quotas

ZFS datasets підтримують задання квот на розмір датасетів – тобто, максимальний розмір, який він може займати.

Документація – Setting Quotas on ZFS File Systems.

Зручно, наприклад, аби якийсь backups/ випадково не забив весь диск.

Є ще dataset reservation, але про це трохи далі.

Задамо ліміт в 500 гігабайт на nas/backups:

root@setevoy-nas:~ # zfs set quota=500G nas/backups

Перевіряємо:

root@setevoy-nas:~ # zfs get quota,used,available nas/backups
NAME         PROPERTY   VALUE  SOURCE
nas/backups  quota      500G   local
nas/backups  used       96K    -
nas/backups  available  500G   -

Налаштування dataset reservation

ZFS дозволяє зарезервувати місце для датасету, гарантуючи доступний простір незалежно від заповненості пулу.

Документація – Setting Reservations on ZFS File Systems.

Важливо, що ZFS Reservation резервує місце незалежно від того, використовується воно чи ні.

Задамо мінімально доступний розмір основного датасету nas/data:

root@setevoy-nas:~ # zfs set reservation=200G nas/data

Перевіряємо:

root@setevoy-nas:~ # zfs get reservation,used,available nas/data
NAME      PROPERTY     VALUE   SOURCE
nas/data  reservation  200G    local
nas/data  used         96K     -
nas/data  available    3.51T   -

Аби змінити reservation чи видалити – просто раз виконуємо zfs set з новим значенням:

root@setevoy-nas:~ # zfs set reservation=100G nas/data
root@setevoy-nas:~ # zfs set reservation=none nas/data

Використання ZFS snapshots

ZFS Snapshots – це миттєві read-only знімки стану dataset, які дозволяють швидко відкотитись до попереднього стану.

Документація – Overview of ZFS Snapshots.

Як це працює, коротко:

  • ZFS працює за принципом COW (Copy On Write), тобто при зміні в блоках даних – зміни робляться в новому блоку, а старі блоки не перезаписуються, поки на них є активні посилання
  • при створенні снапшоту ZFS не копіює дані, а створює таке посилання на цей блок
  • далі, коли ми робимо зміни в даних датасета, для якого є снапшот – то зміни на диску робляться в нових блоках даних, а доступ до старих зберігається через снапшот

Створення snapshots

Перевіряємо на прикладі.

Створимо тестовий файл в /nas/data/:

root@setevoy-nas:/home/setevoy # echo test-snap >> /nas/data/test-snap.txt

Створюємо снапшот:

root@setevoy-nas:/home/setevoy # zfs snapshot nas/data@test-snap

Перевіряємо його:

root@setevoy-nas:/home/setevoy # zfs list -t snapshot nas/data@test-snap
NAME                 USED  AVAIL  REFER  MOUNTPOINT
nas/data@test-snap     0B      -   104K  -

Його атрибути:

root@setevoy-nas:/home/setevoy # zfs get creation,used,referenced nas/data@test-snap
NAME                PROPERTY    VALUE                  SOURCE
nas/data@test-snap  creation    Sat Dec 20 15:46 2025  -
nas/data@test-snap  used        0B                     -
nas/data@test-snap  referenced  104K                   -

Відновлення зі snapshot

Снапшоти зберігаються в каталозі .zfs датасета – /nas/data/.zfs/snapshot/:

root@setevoy-nas:/home/setevoy # ll /nas/data/.zfs/snapshot/test-snap/
total 1
-rw-r--r--  1 root wheel 10 Dec 20 15:45 test-snap.txt

І звідси ми можемо отримати доступ і до нашого тестового файлу:

root@setevoy-nas:/home/setevoy # cat /nas/data/.zfs/snapshot/test-snap/test-snap.txt  
test-snap

Тепер видаляємо оригінальний файл з датасета:

root@setevoy-nas:/home/setevoy # rm /nas/data/test-snap.txt 
root@setevoy-nas:/home/setevoy # file /nas/data/test-snap.txt 
/nas/data/test-snap.txt: cannot open `/nas/data/test-snap.txt' (No such file or directory)

Але в снапшоті він доступний:

root@setevoy-nas:/home/setevoy # cat /nas/data/.zfs/snapshot/test-snap/test-snap.txt 
test-snap

Аби відновити зі снапшоту – можна або просто скопіювати з каталога /nas/data/.zfs/snapshot/test-snap/ з cp, або, якщо треба відкотити весь датасет, то використати zfs rollback – але в такому разі всі зміни, які були зроблені після створення снапшоту будуть втрачені:

root@setevoy-nas:/home/setevoy # zfs rollback nas/data@test-snap

І тепер файл знов на місці:

root@setevoy-nas:/home/setevoy # file /nas/data/test-snap.txt 
/nas/data/test-snap.txt: ASCII text
root@setevoy-nas:/home/setevoy # cat /nas/data/test-snap.txt 
test-snap

Ну і ZFS Boot Environments працюють через ті самі снапшоти – під час виконання freebsd-update install автоматично створюється копія даних, на яку можна відкотитись в разі проблем.

Взагалі ZFS Boot Environments дуже цікава штука, може, окремо про неї напишу.

Замість повного rollback – аби не перезаписувати дані на поточному датасеті – можна зробити клонування снапшоту в новий датасет:

root@setevoy-nas:~ # zfs clone nas/data@test-snap nas/data-restored

Перевіряємо:

root@setevoy-nas:/home/setevoy # zfs list 
NAME                        USED  AVAIL  REFER  MOUNTPOINT
nas                         200G  3.32T   120K  /nas
...
nas/data                    168K  3.51T   104K  /nas/data
nas/data-restored             0B  3.32T   104K  /nas/data-restored
...

І тепер файл доступний тут:

root@setevoy-nas:/home/setevoy # cat /nas/data-restored/test-snap.txt 
test-snap

Видалення snapshot

Для видалення використовуємо zfs destroy:

root@setevoy-nas:/home/setevoy # zfs destroy nas/data@test-snap

Аналогічно видаляються і датасети:

root@setevoy-nas:/home/setevoy # zfs destroy nas/data-restore

Копіювання snapshot

Снапшоти можна передавати між хостами і з zfs receive створювати новий снапшот, можна просто створити tar.gz архів.

Документація і багато прикладів –  Sending and Receiving ZFS Data.

Для передачі використовуємо zfs send, а потім через пайп – отримувача, наприклад – zfs receive:

root@setevoy-nas:/home/setevoy # zfs send nas/data@test-snap | zfs receive nas/backups/data-$(date +%Y%m%d)

Тепер в датасеті nas/backups у нас є новий датасет nas/backups/data-20251221:

root@setevoy-nas:/home/setevoy # zfs list | grep data
nas/backups/data-20251221   104K   500G   104K  /nas/backups/data-20251221
nas/data                    104K  3.51T   104K  /nas/data

В якому міститься копія снапшоту:

root@setevoy-nas:/home/setevoy # ll /nas/backups/data-20251221/.zfs/snapshot/
total 1
drwxr-xr-x  2 root wheel 4 Dec 20 15:45 test-snap

Також можна створювати інкрементальні копії снапшотів з -i та копіювати зашифровані снапшоти.

Якщо в снапшоті конфіденційна інформація, то для zfs send задаємо ключ -w (raw send) – в такому випадку дані передаються в зашифрованому вигляді.

FreeBSD Periodic та автоматизація створення снапшотів

Є кілька утиліт, які допомогають автоматизувати створення снапшотів, з основних – це zfsnap, zfs-periodic (див. більше на ZFS Orchestration Tools – Part 1: Snapshots).

Спробуємо зі zfsnap – встановлюємо його:

root@setevoy-nas:~ # pkg install zfsnap

Важливий нюанс – сам файл називається zfSnap а не zfsnap:

root@setevoy-nas:~ # pkg info -l zfsnap
zfsnap-1.11.1_1:
...
        /usr/local/sbin/zfSnap
...

Разом зі zfsnap додається набір Periodic-файлів:

root@setevoy-nas:/home/setevoy # ll /usr/local/etc/periodic/daily/ | grep Snap
-r-xr-xr-x  1 root wheel 1512 Nov 30 01:57 402.zfSnap
-r-xr-xr-x  1 root wheel 1073 Nov 30 01:57 403.zfSnap_delete

Які по суті являють собою просто shell-скрипти:

root@setevoy-nas:/home/setevoy # cat /usr/local/etc/periodic/daily//402.zfSnap 
#!/bin/sh

# If there is a global system configuration file, suck it in.
#
if [ -r /etc/defaults/periodic.conf ]; then
        . /etc/defaults/periodic.conf
        source_periodic_confs
fi
...

Включити запуск скриптів можна в файлі /etc/periodic.conf (або, краще, /etc/periodic.conf.local):

daily_zfsnap_enable="YES"
daily_zfsnap_recursive_fs="nas/data"
daily_zfsnap_delete_enable="YES"

Запустити виконання всіх daily задач, для яких в /etc/defaults/periodic.conf задано “YES” – з командою periodic:

root@setevoy-nas:/home/setevoy # periodic daily

І тепер  нас є новий снапшот:

root@setevoy-nas:/home/setevoy # zfs list -t snapshot 
NAME                                     USED  AVAIL  REFER  MOUNTPOINT
...
nas/data@daily-2025-12-21_16.41.03--1w     0B      -   104K  -

Моніторинг ZFS

Для моніторингу у нас є цілий набір утиліт – як дефолтні від самої файлової системи, так і додаткові, які можна встановити окремо.

Є класний документ Monitoring ZFS, хоча і 2017 року, але все ще актуальний.

З основного, чим можемо користуватись і що бажано моніторити:

  • SMART: перевірка самих дисків
  • zpool status: перевірка, що нема проблем на самих ZFS pools
  • zfs scrub: не зовсім про моніторинг, але може показати проблеми
  • zpool events: події пулів
  • arcstats: корисно перевіряти ефективність роботи кешу ZFS

Перевірка S.M.A.R.T. для SSD

Диски зовсім нові, але just in case і на майбутнє – налаштуємо S.M.A.R.T. (Self-Monitoring, Analysis, and Reporting Technology).

Встановлюємо пакет:

root@setevoy-nas:~ # pkg install smartmontools

І перевіряємо стан дисків:

root@setevoy-nas:~ # smartctl -a /dev/ada0
root@setevoy-nas:~ # smartctl -a /dev/ada1

Нам цікаві в основному такі показники:

  • SMART overall-health self-assessment test result: PASSED: перевірку пройдено (але зазвичай FAILED тут з’являється, коли все зовсім погано)
  • помилки:
    • Reallocated_Sector_Ct:
      • кількість битих секторів, які диск знайшов і замінив резервними
      • 0 – ідеально, зростання цього значення – поганий сигнал
    • Runtime_Bad_Block:
      • кількість некоректних блоків, знайдених під час нормальної роботи або деградацію лінка (наприклад, падіння швидкості SATA)
      • 0 – ідеально, зростання цього значення – поганий сигнал
    • Uncorrectable_Error_Cnt:
      • кількість помилок читання/запису, які неможливо було виправити
      • 0 – обов’язково має залишатися, зростання – вже серйозна проблема
  • Wear/Used reserve:
    • Wear_Leveling_Count:
      • показує знос елементів памʼяті SSD
      • 0 означає, що диск практично новий або знос мінімальний
    • Used_Rsvd_Blk_Cnt_Tot:
      • скільки резервних блоків уже використано для заміни зношених
      • 0 – ідеальний стан
  • Power_On_Hours:
    • кількість годин, протягом яких диск був увімкнений
    • 83 години – диски тільки нещодавно купив і підключив
  • CRC_Error_Count:
    • кількість помилок передачі даних між диском і контролером (кабель, порт)
    • 0 – норма, зростання часто означає проблеми з кабелем, а не з самим диском
  • Total_LBAs_Written / Host_Writes / NAND_Writes:
    • скільки даних реально записано на диск
    • порівнюємо зі TBW (Total Bytes Written) від виробника
    • в моєму випадку Total_LBAs_Written = 77982, де LBA – це Logical Block Address, який зазвичай SMART рахує по 512 байт, тобто записано на диск ~40 мегабайт – при заявлених Samsung 2400 TB
  • Temperature / Temperature_Celsius / Airflow_Temperature_Cel
    • температура дисків, в мене зараз 28 градусів

SMART Periodic

Для запуска SMART є власні скрипти в /usr/local/etc/periodic.

Аби включити перевірку і репорти – додаємо запуск smartd в автостарт:

root@setevoy-nas:/home/setevoy # sysrc smartd_enable="YES"

І налаштовуємо periodic в /etc/periodic.conf.local.

Аби включити перевірку – треба явно задати диски, по яким цю перевірку робити:

daily_status_smart_devices="/dev/ada0 /dev/ada1"

Результат в репорті:

ZFS Pool Status

zpool status вже запускали вище, тепер додамо запуск по крону і відправку повідомлень.

Скрипти для ZFS /etc/periodic/daily/, наприклад /etc/periodic/daily/404.status-zfs.

Додаємо до /etc/periodic.conf.local:

daily_status_zfs_enable="YES"

Запускаємо скрипти:

root@setevoy-nas:/home/setevoy # periodic daily

Отримуємо листа:

root@setevoy-nas:/home/setevoy # mail -u root
Mail version 8.1 6/6/93.  Type ? for help.
"/var/mail/root": 12 messages 4 new 12 unread
...
 N 12 root@setevoy-nas      Sun Dec 21 17:20  82/3368  "setevoy-nas daily run output"
& 

Читаємо його (вказуємо номер листа – 12, Enter):

(про налаштування пересилки пошти напишу окремо)

ZFS Scrubbing

ZFS Scrubbing – перевірка цілісності даних.

Під час scrub ZFS порівнює checksum кожного блоку зі збереженим значенням і у разі виявлення помилки система фіксує її в логах та, за наявності mirror, автоматично відновлює дані з другої копії.

Оскільки для цього виконується багато I/O операцій, тому не варто запускати scrubbing часто – раз на місяць буде достатньо.

Запускаємо вручну:

root@setevoy-nas:/home/setevoy # zpool scrub nas

І перевіряємо в zpool status:

root@setevoy-nas:/home/setevoy # zpool status
  pool: nas
 state: ONLINE
  scan: scrub repaired 0B in 00:00:00 with 0 errors on Fri Dec 19 16:54:04 2025
config:

        NAME               STATE     READ WRITE CKSUM
        nas                ONLINE       0     0     0
          mirror-0         ONLINE       0     0     0
            gpt/zfs_disk1  ONLINE       0     0     0
            gpt/zfs_disk2  ONLINE       0     0     0

errors: No known data errors

Тепер в scan є “scrub repaired 0B” – все добре.

Скрипт – /etc/periodic/daily/800.scrub-zfs, в якому перевіряється значення daily_scrub_zfs_default_threshold, і, якщо пройшло більше днів, ніж задано в threshold – то запускається zpool scrub.

З daily_scrub_zfs_pools можна вказати, які саме пули перевіряти.

Додаємо до нашого /etc/periodic.conf.local:

...
# SCRUB
daily_scrub_zfs_enable="YES"
daily_scrub_zfs_default_threshold=35
daily_scrub_zfs_pools="nas"
...

ZFS Events та History

З zpool events можна перевірити всі останні події в пулі:

root@setevoy-nas:/home/setevoy # zpool events
TIME                           CLASS
Dec 20 2025 16:46:39.702350180 sysevent.fs.zfs.history_event
Dec 20 2025 16:46:39.711350328 ereport.fs.zfs.config_cache_write
Dec 20 2025 16:46:39.711350328 sysevent.fs.zfs.config_sync
Dec 20 2025 16:46:39.711350328 sysevent.fs.zfs.pool_import
Dec 20 2025 16:46:39.712349914 sysevent.fs.zfs.history_event
Dec 20 2025 16:46:39.720349727 sysevent.fs.zfs.config_sync
Dec 20 2025 14:46:44.749348450 sysevent.fs.zfs.config_sync
...
Dec 21 2025 17:26:30.905209986 sysevent.fs.zfs.history_event

Аби побачити деталі – додаємо -z:

root@setevoy-nas:/home/setevoy #  zpool events -v
TIME                           CLASS
Dec 20 2025 16:46:39.702350180 sysevent.fs.zfs.history_event
        version = 0x0
        class = "sysevent.fs.zfs.history_event"
        pool = "nas"
        pool_guid = 0x2f9ad6b17a5e8426
        pool_state = 0x0
        pool_context = 0x0
        history_hostname = ""
        history_internal_str = "pool version 5000; software version zfs-2.2.7-0-ge269af1b3; uts  14.3-RELEASE 1403000 amd64"
        history_internal_name = "open"
        history_txg = 0x2d65
        history_time = 0x6946b6cf
        time = 0x6946b6cf 0x29dd0364 
        eid = 0x1
...

А з zpool history можна подивитись всі команди, які запускались:

root@setevoy-nas:/home/setevoy # zpool history
History for 'nas':
2025-12-19.13:37:17 zpool create -o ashift=12 -O atime=off -O compression=lz4 -O xattr=sa nas mirror /dev/gpt/zfs_disk1 /dev/gpt/zfs_disk2
2025-12-19.13:41:00 zfs create nas/data
2025-12-19.13:44:19 zfs create nas/backups
2025-12-19.13:48:44 zfs create -o encryption=on -o keyformat=passphrase -o keylocation=prompt nas/private
...
2025-12-21.17:26:30 zfs snapshot -r nas/data@daily-2025-12-21_17.26.30--1w

ZFS I/O statistic

Інформація по I/O операціям з zpool iostat:

root@setevoy-nas:/home/setevoy # zpool iostat
              capacity     operations     bandwidth 
pool        alloc   free   read  write   read  write
----------  -----  -----  -----  -----  -----  -----
nas         1.68M  3.62T      0      0     28    584

ZFS ARC моніторинг

ARC (Adaptive Replacement Cache) – кешування в пам’яті даних, які часто використовуються.

Перевірити поточні значення можна з sysctl:

root@setevoy-nas:/home/setevoy # sysctl kstat.zfs.misc.arcstats.size
kstat.zfs.misc.arcstats.size: 9379192
root@setevoy-nas:/home/setevoy # sysctl kstat.zfs.misc.arcstats.hits
kstat.zfs.misc.arcstats.hits: 26181
root@setevoy-nas:/home/setevoy # sysctl kstat.zfs.misc.arcstats.misses
kstat.zfs.misc.arcstats.misses: 6

Або встановити утиліту zfs-stats:

root@setevoy-nas:/home/setevoy # pkg install zfs-stats

І запустити з -E:

root@setevoy-nas:/home/setevoy # zfs-stats -E

------------------------------------------------------------------------
ZFS Subsystem Report                            Sun Dec 21 18:07:59 2025
------------------------------------------------------------------------

ARC Efficiency:                                 104.72  k
        Cache Hit Ratio:                99.87%  104.58  k
        Cache Miss Ratio:               0.13%   137
        Actual Hit Ratio:               99.87%  104.58  k

        Data Demand Efficiency:         100.00% 0

        CACHE HITS BY CACHE LIST:
          Most Recently Used:           23.21%  24.28   k
          Most Frequently Used:         76.79%  80.30   k
          Most Recently Used Ghost:     0.00%   0
          Most Frequently Used Ghost:   0.00%   0

        CACHE HITS BY DATA TYPE:
          Demand Data:                  0.00%   0
          Prefetch Data:                0.00%   0
          Demand Metadata:              99.96%  104.54  k
          Prefetch Metadata:            0.04%   39

        CACHE MISSES BY DATA TYPE:
          Demand Data:                  0.00%   0
          Prefetch Data:                0.00%   0
          Demand Metadata:              91.24%  125
          Prefetch Metadata:            8.76%   12

Або використати zfs-mon:

Ну і по моніторингу ZFS цього, мабуть, вистачить.

Ще окремо будемо говорити вже про повноцінний моніторинг системи – планую з VictoriaMetrics, і тоді можна буде додати якийсь експортер для ZFS, наприклад zfs_exporter.

Корисні посилання

Loading

FreeBSD: Home NAS, part 4 – локальний DNS з Unbound
0 (0)

21 Грудня 2025

В попередньому пості FreeBSD: WireGuard VPN, Linux peer та routing між мережами підняли VPN для поєднання двох мереж – моєї офісної та домашньої, все працює.

Але зараз, аби підключитись до якогось хосту в мережах треба вказувати IP-адресу.

Можна, звісно, прописувати все в файлах /etc/hosts, але це і не дуже зручно, і будуть клієнти типу Android-телефонів, і взагалі – хочеться все красівоє.

Тому зробимо локальний DNS, на якому буде централізований DNS для всієї інфраструктури:

  • власну локальну DNS-зону .setevoy
  • DNS-відповіді для .setevoy, що залежать від мережі, з якої приходить запит (office/home/VPN)
  • резолвінг зовнішніх доменів через forward-DNS (Cloudflare/Google)

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Вибір DNS серверу

Справа смаку.

Я колись мав BIND, який обслуговував навіть зону самого rtfm.co.ua – але для маленької домашньої мережі це зовсім overkill.

Мав справу з dnsmasq – простий, швидкий, вміє в DHCP, але трохи обмежений в можливостях.

І Unbound – рідний для FreeBSD, теж простий і швидкий, вміє в DNSSEC validation прям з коробки, вміє в views/access-control – те, що треба.

Routers та DHCP зі Static IP

Взагалі, можна було б заморочитись і автоматизувати навіть видачу IP для хостів в мережах, але, по-перше, ну камон – в мене 3 машинки у двох мережах 🙂

По-друге – робочий ноут і ThinkCenter знаходяться в офісі, а домашній ноутбук – вдома. Тобто, робити DHCP на хості з FreeBSD – така собі затєя, бо домашній ноутбук може бути не підключеним до VPN.

Тому – прибиваємо все статично в налаштуваннях роутера (чекаю, чекаю, коли до мене доїде MikroTik hAP ax3!):

Тепер для робочого ноута, коли він підключений по Ethernet, адреса завжди буде 192.168.0.3.

Базовий конфіг Unbound

Встановлюємо пакет:

root@setevoy-nas:/home/setevoy # pkg install -y unbound

Файл налаштувань – /usr/local/etc/unbound/unbound.conf.

Є цікавий приклад конфігу Example of how to configure Unbound as a local forwarder using DNS-over-TLS to forward queries, треба буде глянути.

Документація – unbound.conf і офіційна документація Unbound by NLnet Labs.

Можна забекапити дефолтний конфіг, напишемо свій з нуля:

root@setevoy-nas:/home/setevoy # cp /usr/local/etc/unbound/unbound.conf /usr/local/etc/unbound/unbound.conf-origin

Редагуємо/створюємо /usr/local/etc/unbound/unbound.conf, поки мінімальна конфігурація:

server:
    interface: 0.0.0.0

    access-control: 127.0.0.0/8 allow         # local
    access-control: 192.168.0.0/24 allow      # office LAN
    access-control: 192.168.100.0/24 allow    # home LAN
    access-control: 10.8.0.0/24 allow         # WireGuard VPN

    do-ip6: no
    do-daemonize: yes

    hide-identity: yes
    hide-version: yes

    local-zone: "setevoy." static
    local-data: "nas.setevoy. A 192.168.0.2"
    local-data: "work.setevoy. A 192.168.0.1"
    local-data: "home.setevoy. A 192.168.100.205"

Тут вказуємо:

  • на якій адресі приймати підключення
  • з яких мереж приймати запити
  • відключаємо IPv6 (бо нашо це вдома?)
  • ховаємо ім’я серверу та версію
    • навіть не знав, що так можна – dig CHAOS TXT id.server @192.168.0.2 +short поверне CH TXT "setevoy-nas"
  • і описуємо локальну зону .setevoy з трьома DNS records

Перевіряємо синтаксис конфігу:

root@setevoy-nas:/home/setevoy # unbound-checkconf
unbound-checkconf: no errors in /usr/local/etc/unbound/unbound.conf

Додаємо в автостарт:

root@setevoy-nas:/home/setevoy # sysrc unbound_enable=YES
unbound_enable:  -> YES

Запускаємо:

root@setevoy-nas:/home/setevoy # service unbound start

Перевіряємо локально, з хоста з FreeBSD з drill замість dig:

root@setevoy-nas:/home/setevoy # drill nas.setevoy @127.0.0.1
...
;; ANSWER SECTION:
nas.setevoy.    3600    IN      A       192.168.0.2
...

І якийсь зовнішній домен:

root@setevoy-nas:/home/setevoy # drill google.com @127.0.0.1
...
;; ANSWER SECTION:
google.com.     300     IN      A       142.251.98.101
google.com.     300     IN      A       142.251.98.139
...

Додаємо правило pf для доступу до DNS (див. FreeBSD: знайомство з Packet Filter (PF) firewall):

...
# DNS
pass in on em0 proto { udp tcp } from { 192.168.0.0/24, 192.168.100.0/24, 10.8.0.0/24 } to (em0) port 53 keep state
...

Перевіряємо синтаксис, застосовуємо:

root@setevoy-nas:/home/setevoy # pfctl -nvf /etc/pf.conf && pfctl -f /etc/pf.conf

І пробуємо вже з робочого ноута:

[setevoy@setevoy-work ~]  $ dig nas.setevoy @192.168.0.2 +short
192.168.0.2

[setevoy@setevoy-work ~]  $ dig google.com @192.168.0.2 +short
142.251.98.138
142.251.98.102
...

Задаємо кастомні DNS сервери на офісному роутері:

WireGuard DNS settings

Домашній ноут підключається з WireGuard, де при підключенні можна задавати наш новий DNS.

З дому при підключеному VPN у нас сервер DNS буде на 10.8.0.1:

[setevoy@setevoy-home ~]$ dig work.setevoy @10.8.0.1 +short
192.168.0.1

Редагуємо /etc/wireguard/wg0.conf, в блок [Interface] додаємо DNS:

[Interface]
PrivateKey = 0Cu***UWU=
Address = 10.8.0.3/24
DNS = 10.8.0.1, 192.168.100.1

[Peer]
PublicKey = xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
Endpoint = setevoy-***.ddns.me:51830

AllowedIPs = 10.8.0.1/32, 192.168.0.0/24
PersistentKeepalive = 25

Тепер при підключенні VPN буде задаватись 10.8.0.1 як основний, і адреса домашнього роутера 192.168.100.1 як fallback опція.

Ще можна буде налаштувати split-DNS, задавши Domains = ~setevoy, аби до 10.8.0.1 робити запити тільки для зони .setevoy, див. systemd-resolved та systemd-networkd.

Налаштування forward-only DNS

З поточним конфігом наш unbound виконує повний рекурсивний пошук.

Тобто, коли ми з клієнта робимо запит google.com – то:

  • unbound звертається до root-серверів DNS
  • там шукає Name Servers для зони .com
  • звертається до цих Name Servers, запитуючи інформацію про Name Servers для google.com
  • і тільки після цього йде на Name Servers google.com, звідки бере інформацію по IP, яку потім повертає клієнту

Натомість можемо налаштувати forward-only, і тоді Unbound одним запитом сходить на 1.1.1.1 або 8.8.8.8, і відразу поверне відповідь клієнту.

Додаємо в кінець конфігу:

...

forward-zone:
    name: "."
    forward-addr: 1.1.1.1
    forward-addr: 8.8.8.8

Перевіряємо, застосовуємо зміни:

root@setevoy-nas:/home/setevoy # unbound-checkconf && service unbound reload

Тепер Unbound в порядку пріорітету спочатку перевірить свою локальну зону, якщо google.com в нього на обслуговуванні нема – то він переадресує запит до Cloudflare або Google.

Налаштування логів

Робив для того, аби подивись як впливає налаштування forward-zone – тому теж нехай тут буде.

В блок server: додаємо:

...
server:
    ...

    # enable logging
    verbosity: 3
    log-queries: yes
    log-replies: yes
    logfile: "/var/log/unbound.log"

    # disable caching during tests
    cache-min-ttl: 0
    cache-max-ttl: 0

...

На час тестів можна відключити локальне кешування – cache-min/max-ttl.

По дефолту Unbound працює в chroot-оточенні з кореневою директорією в /usr/local/etc/unbound – створюємо там файл логу:

root@setevoy-nas:/home/setevoy # mkdir -p /usr/local/etc/unbound/var/log
root@setevoy-nas:/home/setevoy # touch /usr/local/etc/unbound/var/log/unbound.log
root@setevoy-nas:/home/setevoy # chown unbound:unbound /usr/local/etc/unbound/var/log/unbound.log
root@setevoy-nas:/home/setevoy # chmod 640 /usr/local/etc/unbound/var/log/unbound.log
root@setevoy-nas:/home/setevoy # unbound-checkconf
unbound-checkconf: no errors in /usr/local/etc/unbound/unbound.conf
root@setevoy-nas:/home/setevoy # service unbound reload

Перевірка роботи Recursive DNS

Тепер відключаємо forward-zone, з робочого ноутбука робимо запит про google.com до нашого DNS:

[setevoy@setevoy-work ~]  $ time dig +short google.com @192.168.0.2
...

real    0m0.109s

Час виконання – 0.109s.

Дивимось лог Unbound:

...
info: 192.168.0.3 google.com. A IN
...
info: priming . IN NS
info: sending query: . NS IN
info: reply from <.> 199.7.83.42#53
info: priming successful for . NS IN
...
info: sending query: com. A IN
info: reply from <.> 202.12.27.33#53
info: query response was REFERRAL
...
info: sending query: google.com. A IN
info: reply from <google.com.> 216.239.38.10#53
info: query response was ANSWER
...
info: 192.168.0.3 google.com. A IN NOERROR 0.101574
...

Тут добре видно, як Unbound працює як рекурсивний резолвер :

  • спочатку виконує запит до root-зони “.“, де отримує Name Servers для зони .com
  • далі звертається до Name Servers зони .com, де отримує адреси Name Servers зони google.com
  • і лише після цього звертається вже до Name Servers домену google.com, отримуючи фінальну A-відповідь з IP-адресами серверів Google

А тепер повертаємо forward-zone, але залишаємо відключеним кешування (cache-min/max-ttl) і робимо запит з клієнта ще раз:

[setevoy@setevoy-work ~]  $ time dig +short google.com @192.168.0.2
...
real    0m0.016s

Тепер час на отримання відповіді – 0.030s, а без forward-zone він був 0.109s (або з логу – google.com. A IN NOERROR 0.101574). Майже в 3 раз швидше. Іноді буває і 0.01s.

І логи Unbound тепер набагато менші:

...
debug: forwarding request
...
debug:    ip4 1.1.1.1 port 53
debug:    ip4 8.8.8.8 port 53
info: sending query: google.com. A IN
debug: sending to target: <.> 8.8.8.8#53
...
info: 192.168.0.3 google.com. A IN NOERROR 0.039006
...

Все, що він зробив – це передав запит до 8.8.8.8 і повернув результат, отриманий від нього.

Unbound DNS views для різних мереж

Зараз у нас з дому, який за VPN, при запиті nas.setevoy повернеться адреса 192.168.0.1:

...
local-data: "nas.setevoy. A 192.168.0.2"
...

Але якщо хочемо ходити чисто через нетворк VPN – можемо розділити зони з views і для кожної мережі створити власні зони:

server:
    interface: 0.0.0.0
    do-daemonize: yes
    do-ip6: no

    hide-identity: yes
    hide-version: yes

    # map client networks to views
    access-control-view: 127.0.0.0/8 office
    access-control-view: 192.168.0.0/24 office
    access-control-view: 10.8.0.0/24 vpn

#######################
### Hosts list note ###
#######################
# -Office: 
#  - setevoy-work: Arch Linux laptop
#  - setevoy-pc: Arch Linux/Windows gaming PC
#  - setevoy-nas: FreeBSD ThinkCentre
#  - TP-Link router
# - Home:
#  - setevoy-home: Arch Linux laptop
#  - TP-Link router

################
# OFFICE VIEW #
################
view:
    name: "office"
    local-zone: "setevoy." static
    local-data: "nas.setevoy. A 192.168.0.2"
    local-data: "work.setevoy. A 192.168.0.3"
    local-data: "pc.setevoy. A 192.168.0.4"

#############
# VPN VIEW #
#############
view:
    name: "vpn"
    local-zone: "setevoy." static
    local-data: "nas.setevoy. A 10.8.0.1"
    # not VPN-connected, but Home has routing to the 192.168.0.0/24
    local-data: "work.setevoy. A 192.168.0.3"
    local-data: "pc.setevoy. A 192.168.0.4"

#################
# FORWARD ONLY #
#################
forward-zone:
    name: "."
    forward-addr: 1.1.1.1
    forward-addr: 8.8.8.8

Тут в access-control-view: 10.8.0.0/24 vpn прив’язує клієнтів з VPN-мережі 10.8.0.0/24 до view з ім’ям vpn, для якого далі окремо описується DNS-логіка.

Конкретно в моєму сетапі домашній ноутбук все одно має роутинг в мережу 192.168.0.0/24 через 10.8.0.1, і можна повертати адреси з неї – але в цілому штука корисна.

Перевіряємо, застосовуємо:

root@setevoy-nas:/home/setevoy # unbound-checkconf && service unbound reload
unbound-checkconf: no errors in /usr/local/etc/unbound/unbound.conf

Перевіряємо з дому – отримуємо адресу з мережі VPN:

[setevoy@setevoy-home ~] $ dig nas.setevoy @10.8.0.1 +short
10.8.0.1

І з офісу – результат буде з офісної мережі:

[setevoy@setevoy-work ~] $ dig nas.setevoy @192.168.0.2 +short
192.168.0.2

Готово.

Loading

FreeBSD: Home NAS, part 3 – WireGuard VPN, Linux peer та routing
5 (1)

18 Грудня 2025

Продовжую налаштування свого домашнього сервера на FreeBSD 14.3, де планується мати NAS.

В попередньому пості FreeBSD: знайомство з Packet Filter (PF) firewall познайомились з фаєрволами, наступний крок – це налаштувати VPN для доступу.

Основна ідея – поєднати (нарешті!) мій “офіс” і квартиру, а пізніше, можливо, ще і підключити сервер, на якому зараз працює rtfm.co.ua – аби бекапи блогу і баз даних зберігати відразу на ZFS пулі з RAID домашнього сервера.

WireGuard vs OpenVPN

Коли діло дійшло до вибору який конкретно VPN сервер вибрати, то я спочатку думав взяти OpenVPN – бо працюю з ним не один рік, і на RTFM про нього навіть є якісь матеріали.

Але, трохи подумавши вирішив, що для домашнього VPN рішення типу OpenVPN або Pritunl будуть трохи оверхедом, і можна спробувати WireGuard.

Системи дуже різні, але якщо коротко, то:

  • WireGuard набагато менший по коду – наприклад, Linux-реалізація це близько 4000 строк в ядрі, тоді як в OpenVPN це близько 100,000 строк в user space
  • WireGuard працює як модуль ядра – обробка пакетів і криптографія виконуються безпосередньо в kernel space, а OpenVPN є user space сервісом і працює через TCP або UDP socket та взаємодіє з ядром через стандартний мережевий стек ядра
  • туди ж – шифрування, бо WireGuard має вбудовану криптографію, яка є частиною самого протоколу і працює в kernel space, а OpenVPN використовує стандартний SSL/TLS стек (OpenSSL, LibreSSL тощо) в user space, що додає складність і накладні витрати CPU/RAM
  • модель роботи WireGuard – peer-to-peer, тобто протокол не має вбудованих ролей “сервер” чи “клієнт”, є лише Peers з ключами і дозволеними IP, тоді як OpenVPN побудований навколо класичної клієнт-серверної архітектури

В результаті WireGuard можна сприймати не як окремий сервіс, а як зашифрований мережевий інтерфейс, тоді як OpenVPN залишається класичним прикладним VPN-сервісом.

Навіть офіційний документ по WireGuard названий “Next Generation Kernel Network Tunnel“.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Архітектура мережі

Отже, що в мене є:

  • “офіс”: окрема локальна мережа 192.168.0.0/24, на вході – роутер TP-LINK Archer AX12
    • в цій мережі є робочий ноутбук з Arch Linux і Lenovo ThinkCentre з FreeBSD
    • на FreeBSD буде NAS, NFS і, власне, саме WireGuard
      • хоча Archer AX12 має власні вбудовані OpenVPN і WireGuard – але хочеться зробити самому, руками, плюс все ж більше контролю
  • дома: там мережа 192.168.100.0/24, на вході точно такий же роутер Archer AX12
    • там єдиний клієнт – це домашній ноутбук з Arch Linux

І що я хочу зробити:

  • на FreeBSD буде WireGuard в ролі VPN-сервера
  • на роутері Archer AX12 буде NAT port-forwarding для підключення до WireGuard на FreeBSD
  • мережа VPN – 10.8.0.1/24
  • на FreeBSD – Paket Filter firewall для контролю трафіка
  • обидва ноутбуки повинні мати доступ один до одного і до майбутнього NAS на FreeBSD

Як це в результаті виглядає схематично:

Запуск WireGuard на FreeBSD

У FreeBSD (власне, як і в Linux) WireGuard – це kernel module + userspace tools: основна “робоча” частина завантажується як модуль ядра, а для роботи з ним встановлюється окремий пакет.

Встановлюємо wireguard-tools:

root@setevoy-nas:/home/setevoy # pkg install wireguard-tools

Завантажуємо модуль:

root@setevoy-nas:/home/setevoy # kldload if_wg

Перевіряємо:

root@setevoy-nas:/home/setevoy # kldstat | grep wg
 8    1 0xffffffff82a47000    2f5c0 if_wg.ko

Додаємо запуск WireGuard в /etc/rc.conf:

root@setevoy-nas:/home/setevoy # sysrc wireguard_enable=YES
wireguard_enable:  -> YES
root@setevoy-nas:/home/setevoy # sysrc wireguard_interfaces=wg0
wireguard_interfaces:  -> wg0

Поки не запускаємо – переходимо до налаштування мережі.

Network configuration

Далі треба налаштувати систему на роутинг пакетів між фізичним інтерфейсом та інтерфейсом WireGuard, і оновити конфіг фаєрвола.

Конфігурація IP forwarding

Додаємо IP forwarding з інтерфейсу wg0 (якого ще нема, з’явиться під час запуску WireGuard) на інтерфейс LAN, em0.

Оновлюємо автозапуск в /etc/rc.conf:

root@setevoy-nas:/usr/local/etc/wireguard # sysrc gateway_enable="YES"
gateway_enable: NO -> YES

Аби переадресація запрацювала зараз, без ребуту – вмикаємо її одразу з sysctl:

root@setevoy-nas:/usr/local/etc/wireguard # sysctl net.inet.ip.forwarding=1
net.inet.ip.forwarding: 0 -> 1

Перевіряємо:

root@setevoy-nas:/usr/local/etc/wireguard # sysctl net.inet.ip.forwarding
net.inet.ip.forwarding: 1

Наступний крок – налаштування Packet Filter.

Конфігурація Packet Filter

Отже, що у нас є:

  • мережа VPN: 10.8.0.0/24
  • мережа офісу, де FreeBSD/VPN: 192.168.0.0/24
    • FreeBSD LAN IP: 192.168.0.2
  • роутити інтернет через VPN не потрібно – тільки трафік між мережами дома і офісу

Конфіг pf зараз – мінімалістичний, з попереднього поста:

allowed_tcp_ports = "{ 22 }"

allowed_clients = "{ 192.168.0.0/24, 192.168.1.0/24 }"

set skip on lo

block all

# allow ssh only from specific hosts
pass in proto tcp from $allowed_clients to any port $allowed_tcp_ports keep state

# allow all outgoing traffic
pass out all keep state

Що до нього треба додати:

  • дозволити вхідні UDP-з’єднання на порт WireGuard (51820) для handshake
  • дозволити трафік з VPN-мережі 10.8.0.0/24 до самого FreeBSD-хоста (ping, SSH)
  • дозволити транзитний трафік з VPN-мережі 10.8.0.0/24 до локальних мереж офісу і дому (192.168.0.0/24 та 192.168.100.0/24)
  • дозволити ICMP і SSH з VPN-мережі та домашньої мережі до FreeBSD-хоста
  • дозволити вихідний трафік з FreeBSD

Я додав макроси в конфіг, але поки пишу і тестую – вказую всі порти та адреси явно прямо в конфігу, простіше читати.

Тепер /etc/pf.conf буде виглядати так:

##################
### Interfaces ###
##################
# lan_if = "em0"
# wg_if  = "wg0"

################
### Networks ###
################
# lan_net      = "192.168.0.0/24"
# home_net     = "192.168.100.0/24"
# wg_net       = "10.8.0.0/24"
# vpn_nets     = "{ 10.8.0.0/24, 192.168.100.0/24 }"

################
### Services ###
################
# ssh_ports = "{ 22 }"
# wg_port   = "51820"

######################
### Basic settings ###
######################

# do not filter loopback traffic
set skip on lo

######################
### Default policy ###
######################

# block everything by default
block all

#######################
### Inbound traffic ###
#######################

### SSH

# allow SSH from Office LAN (192.168.0.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.0.0/24 to (em0) port 22 keep state

# allow SSH from Home network (192.168.100.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.100.0/24 to (em0) port 22 keep state

# allow SSH from VPN clients to FreeBSD host
pass in on wg0 proto tcp from 10.8.0.0/24 to (wg0) port 22 keep state

### VPN 

# allow WireGuard handshake (UDP/51820) on LAN interface
pass in on em0 proto udp to (em0) port 51820 keep state

# allow VPN clients (10.8.0.0/24) to access FreeBSD host itself
# this allows ping, ssh, etc. to the wg0 address
pass in on wg0 from 10.8.0.0/24 to (wg0) keep state

# allow VPN clients to access Office LAN (192.168.0.0/24)
pass in on wg0 from 10.8.0.0/24 to 192.168.0.0/24 keep state

# allow VPN clients to access Home network (192.168.100.0/24)
pass in on wg0 from 10.8.0.0/24 to 192.168.100.0/24 keep state

# allow ICMP (ping) from VPN clients to FreeBSD host
pass in on wg0 proto icmp from 10.8.0.0/24 to (wg0) keep state

# allow ICMP (ping) from Home network to FreeBSD host
pass in on em0 proto icmp from 192.168.100.0/24 to (em0) keep state

############################
### outbound traffic ###
############################

# allow all outbound traffic from FreeBSD
pass out keep state

Перевіряємо синтаксис:

root@setevoy-nas:/home/setevoy # pfctl -vnf /etc/pf.conf
set skip on { lo }
block drop all
pass in log on em0 inet proto tcp from 192.168.0.0/24 to (em0) port = ssh flags S/SA keep state
pass in log on em0 inet proto tcp from 192.168.100.0/24 to (em0) port = ssh flags S/SA keep state
pass in on wg0 inet from 10.8.0.0/24 to (wg0) flags S/SA keep state
pass in on wg0 inet proto icmp from 10.8.0.0/24 to (wg0) keep state
pass in on wg0 inet from 10.8.0.0/24 to 192.168.0.0/24 flags S/SA keep state
pass in on wg0 inet from 10.8.0.0/24 to 192.168.100.0/24 flags S/SA keep state
pass in on em0 inet proto icmp from 192.168.100.0/24 to (em0) keep state
pass in on em0 proto udp from any to (em0) port = 51820 keep state
pass out all flags S/SA keep state

Виконуємо reload:

root@setevoy-nas:/home/setevoy # service pf reload
Reloading pf rules.

Тепер можемо підготувати запуск WireGuard.

Конфігурація WireGuard

Тут все дуже просто – створити ключі, написати конфіг-файл.

Створення ключів

Комунікація і криптографія у WireGuard побудована на стандартній схемі асиметричних ключів:

  • на “сервері” зберігається приватний ключі
  • на клієнті вказується публічний
  • під час handshake клієнт переконується, що підключився саме до того сервера, публічний ключ якого він знає
  • а далі, з цими ключами, відбувається і шифрування даних

Див. Key Exchange and Data Packets.

Слово “сервер” все ж беру в лапки, бо, як було зазначено вище – WireGuard це P2P, а не client-server.

Після установки wireguard-tools створюється каталог /usr/local/etc/wireguard – переходимо туди, і з wg genkey створюємо приватний та публічний ключ:

root@setevoy-nas:/home/setevoy # cd /usr/local/etc/wireguard
root@setevoy-nas:/usr/local/etc/wireguard # wg genkey | tee server.key | wg pubkey > server.pub

Міняємо права доступу до приватного ключа:

root@setevoy-nas:/usr/local/etc/wireguard # chmod 600 server.key

Перевіряємо:

root@setevoy-nas:/usr/local/etc/wireguard # chmod 600 server.key 
root@setevoy-nas:/usr/local/etc/wireguard # ll
total 12
-rw-------  1 root wheel  45 Dec 17 15:58 server.key
-rw-r--r--  1 root wheel  45 Dec 17 15:58 server.pub

Базовий конфіг для WireGuard

Можна створити кілька різних конфігурацій в /usr/local/etc/wireguard/, кожен на власному порті та/або IP та з власним ключем і мати кілька різних VPN-підключень, а керувати ними використовуючи ім’я файлу – wg0, wg1, etc.

Є навіть генератори конфігу – https://www.wireguardconfig.com.

Документація по синтаксису – Wireguard Configuration File Format.

Отримуємо приватний ключ:

root@setevoy-nas:/usr/local/etc/wireguard # cat server.key 
cLS***GQ=

Створюємо файл /usr/local/etc/wireguard/wg0.conf – поки тільки “сервер”:

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = cLS***sGQ=

Блок Interface визначає параметри WireGuard-інтерфейсу wg0 – його IP-адресу, UDP-порт і приватний ключ, який використовується для шифрування трафіку.

Тут жеж можна вказати які DNS використовувати, чи робити апдейт в таблицях маршрутизації клієнтів (default – true) і запуск скриптів з PreUp, PostUp, PreDown, PostDown.

Запускаємо сам WireGuard:

root@setevoy-nas:/home/setevoy # wg-quick up wg0
[#] ifconfig wg create name wg0
[#] wg setconf wg0 /dev/stdin
[#] ifconfig wg0 inet 10.8.0.1/24 alias
[#] ifconfig wg0 mtu 1420
[#] ifconfig wg0 up
[+] Backgrounding route monitor

Перевіряємо інтерфейс:

root@setevoy-nas:/home/setevoy # ifconfig wg0
wg0: flags=10080c1<UP,RUNNING,NOARP,MULTICAST,LOWER_UP> metric 0 mtu 1420
        options=80000<LINKSTATE>
        inet 10.8.0.1 netmask 0xffffff00
        groups: wg
        nd6 options=109<PERFORMNUD,IFDISABLED,NO_DAD>

Та статус WireGuard:

root@setevoy-nas:/home/setevoy # wg show
interface: wg0
  public key: xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
  private key: (hidden)
  listening port: 51820

Поки у нас нема ніяких клієнтів – переходимо до них.

TP-Link Dynamic DNS та NAT port-forwarding

Аби з дому підключатись до хосту з FreeBSD і WireGuard – на роутері в офісі додаємо форвард портів:

  • protocol: UDP
  • зовнішній порт на роутері: 51830 (трохи замаскувати від ботів)
  • куди форвардити: 192.168.0.2 (хост з FreeBSD)
  • на який порт форвардити: 51830 (WireGuard на em0 на FreeBSD)

На TP-Link Archer AX12 це виглядає так:

Якщо Internet IP в офісі динамічний – в Archer AX12 є можливість налаштування Dynamic DNS:

Хоча в мене він статичний, але DDNS заради інтересу налаштував з https://www.noip.com.

Запуск WireGuard на Arch Linux

В Linux все аналогічно – в ядрі є модулі, нам треба тільки встановити пакет з утилітами.

Перевіряємо модулі:

root@setevoy-home:/home/setevoy # lsmod | grep wireguard
wireguard             122880  0
curve25519_x86_64      36864  1 wireguard
libcurve25519_generic    45056  2 curve25519_x86_64,wireguard
ip6_udp_tunnel         16384  1 wireguard
udp_tunnel             32768  1 wireguard

Встановлюємо пакет:

root@setevoy-home:/home/setevoy # pacman -S wireguard-tools

Переходимо в /etc/wireguard/, створюємо ключі:

root@setevoy-home:/home/setevoy # cd /etc/wireguard/
root@setevoy-home:/etc/wireguard # wg genkey | tee client1.key | wg pubkey > client1.pub

Міняємо права доступу на приватний ключ:

root@setevoy-home:/etc/wireguard # chmod 600 client1.key

Тепер можемо додавати Peers – клієнтів.

Для цього нам потрібно додати ключі на клієнті та на сервері:

  • на сервері:
    • в InterfacePrivateKey: це /usr/local/etc/wireguard/server.key на хості FreeBSD
    • в PeerPublicKey: це /etc/wireguard/client1.pub на ноутбуці з Arch Linux
  • на клієнті:
    • в InterfacePrivateKey: це /etc/wireguard/client1.key
    • в PeerPublicKey: це /usr/local/etc/wireguard/server.pub

Описуємо конфіг на клієнті, файл /etc/wireguard/wg0.conf:

[Interface]
PrivateKey = 0Cu***UWU=
Address = 10.8.0.3/24

[Peer]
PublicKey = xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
Endpoint = setevoy-***.ddns.me:51830

AllowedIPs = 10.8.0.1/32, 192.168.0.0/24
PersistentKeepalive = 25

Тут в AllowedIPs вказуємо мережі в які, по-перше, буде доступ взагалі, по-друге – вони будуть додані в таблиці маршрутизації (“Acts as a routing table and access control list“).

Запускаємо на клієнті:

[root@setevoy-wg-test setevoy]# wg-quick up wg0
[#] ip link add dev wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.8.0.3/24 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] ip -4 route add 10.8.0.3/32 dev wg0
[#] ip -4 route add 192.168.0.0/24 dev wg0

Тут:

  • ip -4 address add: Interface - Address заданий для wg0
  • ip -4 route add 10.8.0.3/32 та 192.168.0.0/24: додані нові роути через інтерфейс wg0 для мереж VPN та офісної локалки

Перевіряємо:

root@setevoy-home:/etc/wireguard # ip r s 10.8.0.0/24
10.8.0.0/24 dev wg0 proto kernel scope link src 10.8.0.3 
root@setevoy-home:/etc/wireguard # ip r s 192.168.0.0/24
192.168.0.0/24 dev wg0 scope link

Додаємо Peer на сервері, файл /usr/local/etc/wireguard/wg0.conf тепер буде таким:

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = cLS***sGQ=

[Peer]
PublicKey = d7yqxOky4qOI/NTl/qbUnijfICwmbe/e/ulSVuQKLhk=
AllowedIPs = 10.8.0.3/32, 192.168.100.0/24

Перезапускаємо:

root@setevoy-nas:/usr/local/etc/wireguard # wg-quick down wg0
[#] ifconfig wg0 destroy

root@setevoy-nas:/usr/local/etc/wireguard # wg-quick up wg0
[#] ifconfig wg create name wg0
[#] wg setconf wg0 /dev/stdin
[#] ifconfig wg0 inet 10.8.0.1/24 alias
[#] ifconfig wg0 mtu 1420
[#] ifconfig wg0 up
[#] route -q -n add -inet 10.8.0.2/32 -interface wg0
[+] Backgrounding route monitor

Перевіряємо статус на клієнті:

root@setevoy-home:/etc/wireguard # wg show
interface: wg0
  public key: d7yqxOky4qOI/NTl/qbUnijfICwmbe/e/ulSVuQKLhk=
  private key: (hidden)
  listening port: 36864

peer: xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
  endpoint: 178.***.***.184:51830
  allowed ips: 10.8.0.1/32, 192.168.0.0/24
  latest handshake: 1 minute, 44 seconds ago
  transfer: 4.35 KiB received, 5.84 KiB sent
  persistent keepalive: every 25 seconds

Головне, на що звертаємо увагу, це “latest handshake” – значить клієнт до сервера підєднався.

Перевіряємо на сервері:

root@setevoy-nas:/home/setevoy # wg show
interface: wg0
  public key: xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
  private key: (hidden)
  listening port: 51820

peer: d7yqxOky4qOI/NTl/qbUnijfICwmbe/e/ulSVuQKLhk=
  endpoint: 178.***.***.236:56432
  allowed ips: 192.168.100.0/24, 10.8.0.3/32
  latest handshake: 15 seconds ago
  transfer: 1.69 KiB received, 3.87 KiB sent

Перевіряємо SSH з клієнта на сервер:

root@setevoy-home:/etc/wireguard # ssh [email protected]
([email protected]) Password for setevoy@setevoy-nas:
...
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!
...

setevoy@setevoy-nas:~ $

Або:

[setevoy@setevoy-home ~]$ ssh 192.168.0.2
([email protected]) Password for setevoy@setevoy-nas:

Додаємо профайл wg0 в автозапуск:

[setevoy@setevoy-home ~]$ sudo systemctl enable wg-quick@wg0
Created symlink '/etc/systemd/system/multi-user.target.wants/[email protected]' → '/usr/lib/systemd/system/[email protected]'.

В принципі, на цьому вже майже все готове – доступ є, все працює.

Але чого хочеться ще – це мати прямий доступ з домашнього ноута на робочий і з робочого на домашній, бо на робочому ноуті VPN нема – він там і не потрібен, бо FreeBSD/NAS в тій самій локальній мережі.

Конфігурація cross-LAN доступу

Тож що треба зробити – це налаштувати прямий доступ між ноутами в домашній мережі 192.168.100.0/24 і офісній 192.168.0.0/24, бо зараз з робочого ноутбука на ноутбук вдома і навпаки доступ не працює.

Картина зараз така:

  • IP ноута в офісі: 192.168.0.165
  • IP ноута вдома: 192.168.100.205
  • на робочому ноуті WireGuard нема
  • з офісу на домашній ноут конекта нема
  • з дому на робочий ноут конекта нема
  • з дому на FreeBSD конект є

Налаштування Routing tables

Поки робимо – закоментуємо block all в /etc/pf.conf, потім до нього повернемось.

В результаті того, що зараз будемо робити – вийде ось така схема: тут головне – це маршрути, спеціально робив схемою, аби те, що буде описано далі було простіше зрозуміти:

Перевіряємо роути з домашнього ноута на FreeBSD:

root@setevoy-home:/etc/wireguard # ip route get 192.168.0.2
192.168.0.2 dev wg0 src 10.8.0.3 uid 0

І на робочий ноут:

root@setevoy-home:/etc/wireguard # ip route get 192.168.0.165
192.168.0.165 dev wg0 src 10.8.0.3 uid 0

Трафік йде через wg0, і Source Address для пакета задається як 10.8.0.3.

А на робочому ноуті роут на домашній ноут йде через 192.168.0.1:

[setevoy@setevoy-work ~] $ ip route get 192.168.100.205
192.168.100.205 via 192.168.0.1 dev wlan0 src 192.168.0.165 uid 1000

Тут 192.168.0.1 – дефолтний гейтвей, роутер в офісі, який нічого не знає про домашню мережу 192.168.100.0/24.

Тому перше – додаємо роут в домашню мережу через хост з FreeBSD:

[setevoy@setevoy-work ~] $ sudo ip route add 192.168.100.0/24 via 192.168.0.2

Перевіряємо ще раз:

[setevoy@setevoy-work ~] $ ip route get 192.168.100.205
192.168.100.205 via 192.168.0.2 dev wlan0 src 192.168.0.165 uid 1000

Тепер є контакт з офісу додому:

[setevoy@setevoy-work ~] $ ping 192.168.100.205 -c 1
PING 192.168.100.205 (192.168.100.205) 56(84) bytes of data.
64 bytes from 192.168.100.205: icmp_seq=1 ttl=63 time=62.0 ms

--- 192.168.100.205 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

Але з домашнього все ще нема, бо з дому ми шлемо:

  • з домашнього ноута з IP 192.168.100.205
    • через FreeBSD з IP 192.168.0.2
      • на робочий ноутбук з IP 192.168.0.165

Але у нас з домашнього ноутбука задається Source IP як 10.8.0.3:

root@setevoy-home:/etc/wireguard # ip route get 192.168.0.165
192.168.0.165 dev wg0 src 10.8.0.3 uid 0

Бо маршрут в 192.168.0.0/24 заданий через VPN інтерфейс wg0:

root@setevoy-home:/etc/wireguard # ip r s 192.168.0.0/24
192.168.0.0/24 dev wg0 scope link 

А у wg0 заданий IP 10.8.0.3:

root@setevoy-home:/etc/wireguard # ip a s wg0
20: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none 
    inet 10.8.0.3/24 scope global wg0

А робочий ноут нічого про мережу 10.8.0.0/24 не знає і не може повернути відповідь.

Тому на робочому ноуті додаємо ще один маршрут:

[setevoy@setevoy-work ~] $ sudo ip route add 10.8.0.0/24 via 192.168.0.2 dev wlan0

Перевіряємо:

[setevoy@setevoy-work ~]  $ ip r s 10.8.0.0/24
10.8.0.0/24 via 192.168.0.2 dev wlan0

І тепер з домашнього ноута на робочий доступ теж є:

root@setevoy-home:/etc/wireguard # ping -c1 192.168.0.165
PING 192.168.0.165 (192.168.0.165) 56(84) bytes of data.
64 bytes from 192.168.0.165: icmp_seq=1 ttl=63 time=6.19 ms

--- 192.168.0.165 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

Аби ці роути додати постійно – можна зробити з NetworkManager CLI.

Видаляємо те, що додали вручну:

[setevoy@setevoy-work ~] $ sudo ip route del 10.8.0.0/24 via 192.168.0.2
[setevoy@setevoy-work ~] $ sudo ip route del 192.168.100.0/24 via 192.168.0.2

Знаходимо ім’я підключення:

[setevoy@setevoy-work ~] $ nmcli connection show
NAME                  UUID                                  TYPE       DEVICE          
setevoy-tp-link-21-5  3a12a60d-7b37-4c20-b573-d27c47a94ae5  wifi       wlan0 
...

Додаємо роути:

[setevoy@setevoy-work ~] $ nmcli connection modify setevoy-tp-link-21-5 +ipv4.routes "10.8.0.0/24 192.168.0.2,192.168.100.0/24 192.168.0.2"

Перевіряємо:

[setevoy@setevoy-work ~] $ nmcli connection show setevoy-tp-link-21-5 | grep ipv4.routes
ipv4.routes:                            { ip = 10.8.0.0/24, nh = 192.168.0.2 }; { ip = 192.168.100.0/24, nh = 192.168.0.2 }

Перезапускаємо підключення:

[setevoy@setevoy-work ~] $ sudo nmcli connection down setevoy-tp-link-21-5 && sudo nmcli connection up setevoy-tp-link-21-5
Connection 'setevoy-tp-link-21-5' successfully deactivated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/15)
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/16)

Перевіряємо роути тепер:

[setevoy@setevoy-work ~] $ ip route get 10.8.0.3
10.8.0.3 via 192.168.0.2 dev wlan0 src 192.168.0.165 uid 1000

[setevoy@setevoy-work ~] $ ip route get 192.168.100.205
192.168.100.205 via 192.168.0.2 dev wlan0 src 192.168.0.165 uid 1000

Тепер у нас є ping з офісного ноутбука на домашній:

[setevoy@setevoy-work ~] $ ping -c1 192.168.100.205
PING 192.168.100.205 (192.168.100.205) 56(84) bytes of data.
64 bytes from 192.168.100.205: icmp_seq=1 ttl=63 time=5.95 ms

--- 192.168.100.205 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

І з домашнього на робочий:

root@setevoy-home:/etc/wireguard # ping -c1 192.168.0.165
PING 192.168.0.165 (192.168.0.165) 56(84) bytes of data.
64 bytes from 192.168.0.165: icmp_seq=1 ttl=63 time=5.67 ms

--- 192.168.0.165 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

Налаштування Packet Filter

Але якщо ми включимо block all в pf, то підключення з офісу на домашній ноут зламається, бо у нас зараз правила тільки для FreeBSD host:

...
# allow SSH from Office LAN (192.168.0.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.0.0/24 to (em0) port 22 keep state

...
# allow ICMP (ping) from Home network to FreeBSD host
pass in on em0 proto icmp from 192.168.100.0/24 to (em0) keep state

...

Тут:

  • перше правило – дозволяє SSH з офісної мережі на IP інтерфейсу em0 хоста з FreeBSD
  • друге – дозволяє ping з домашньої мережі на IP інтерфейсу em0 хоста з FreeBSD

Тому додаємо ще два правила – з SSH і ping з офісу додому:

...
# allow SSH from Office network to Home network
pass in on em0 proto tcp from 192.168.0.0/24 to 192.168.100.0/24 port 22 keep state

...

# allow ICMP from Home network to Office network
pass in on em0 proto icmp from 192.168.0.0/24 to 192.168.100.0/24 keep state
...

Перевіряємо, перечитуємо конфіг pf:

root@setevoy-nas:/usr/local/etc/wireguard # pfctl -vnf /etc/pf.conf && service pf reload
set skip on { lo }
block drop log all
pass in log on em0 inet proto tcp from 192.168.0.0/24 to (em0) port = ssh flags S/SA keep state
pass in log on em0 inet proto tcp from 192.168.100.0/24 to (em0) port = ssh flags S/SA keep state
pass in on wg0 inet from 10.8.0.0/24 to (wg0) flags S/SA keep state
pass in on wg0 inet proto icmp from 10.8.0.0/24 to (wg0) keep state
pass in on wg0 inet from 10.8.0.0/24 to 192.168.0.0/24 flags S/SA keep state
pass in on wg0 inet from 10.8.0.0/24 to 192.168.100.0/24 flags S/SA keep state
pass in on em0 inet proto tcp from 192.168.0.0/24 to 192.168.100.0/24 port = ssh flags S/SA keep state
pass in on em0 inet proto icmp from 192.168.0.0/24 to 192.168.100.0/24 keep state
pass in on em0 proto udp from any to (em0) port = 51820 keep state
pass out all flags S/SA keep state
Reloading pf rules.

І тепер у нас є пінг з дому в офіс:

root@setevoy-home:/etc/wireguard # ping -c1 192.168.0.165
PING 192.168.0.165 (192.168.0.165) 56(84) bytes of data.
64 bytes from 192.168.0.165: icmp_seq=1 ttl=63 time=8.09 ms

--- 192.168.0.165 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

Є SSH ssh з дому в офіс:

root@setevoy-home:/etc/wireguard # ssh 192.168.0.165
[email protected]'s password:

Є пінг з офісу додому:

[setevoy@setevoy-work ~]  $ ping -c1 192.168.100.205
PING 192.168.100.205 (192.168.100.205) 56(84) bytes of data.
64 bytes from 192.168.100.205: icmp_seq=1 ttl=63 time=60.5 ms

--- 192.168.100.205 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

І є SSH з офісу додому:

[setevoy@setevoy-work ~]  $ ssh 192.168.100.205
[email protected]'s password: 

Все працює.

Весь конфіг /etc/pf.conf тепер такий:

##################
### Interfaces ###
##################
# lan_if = "em0"
# wg_if  = "wg0"

################
### Networks ###
################
# lan_net      = "192.168.0.0/24"
# home_net     = "192.168.100.0/24"
# wg_net       = "10.8.0.0/24"
# vpn_nets     = "{ 10.8.0.0/24, 192.168.100.0/24 }"

################
### Services ###
################
# ssh_ports = "{ 22 }"
# wg_port   = "51820"

######################
### Basic settings ###
######################

# do not filter loopback traffic
set skip on lo

######################
### Default policy ###
######################

# block everything by default
block log all

#######################
### Inbound traffic ###
#######################

### SSH

# allow SSH from Office LAN (192.168.0.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.0.0/24 to (em0) port 22 keep state

# allow SSH from Home network (192.168.100.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.100.0/24 to (em0) port 22 keep state

# allow SSH from VPN clients to FreeBSD host
pass in on wg0 proto tcp from 10.8.0.0/24 to (wg0) port 22 keep state

### NEW
# allow SSH from Office netwrok to Home network 
pass in on em0 proto tcp from 192.168.0.0/24 to 192.168.100.0/24 port 22 keep state

### TEST

# allow Office LAN to reach Home LAN via WireGuard
#pass in  on em0 from 192.168.0.0/24 to 192.168.100.0/24 keep state
#pass out on wg0 from 192.168.0.0/24 to 192.168.100.0/24 keep state

# allow Home LAN to reach Office LAN via WireGuard
#pass in  on wg0 from 192.168.100.0/24 to 192.168.0.0/24 keep state
#pass out on em0 from 192.168.100.0/24 to 192.168.0.0/24 keep state

### VPN 

# allow WireGuard handshake (UDP/51820) on LAN interface
pass in on em0 proto udp to (em0) port 51820 keep state

# allow VPN clients (10.8.0.0/24) to access FreeBSD host itself
# this allows ping, ssh, etc. to the wg0 address
pass in on wg0 from 10.8.0.0/24 to (wg0) keep state

# allow VPN clients to access Office LAN (192.168.0.0/24)
pass in on wg0 from 10.8.0.0/24 to 192.168.0.0/24 keep state

# allow VPN clients to access Home network (192.168.100.0/24)
pass in on wg0 from 10.8.0.0/24 to 192.168.100.0/24 keep state

# 
#pass in on em0 from 192.168.0.0/24 to 192.168.100.0/24 keep state

#pass in on wg0 from 192.168.100.0/24 to 192.168.0.0/24 keep state

### ICMP

# allow ICMP from VPN clients to FreeBSD host
pass in on wg0 proto icmp from 10.8.0.0/24 to (wg0) keep state

# allow ICMP from Home network to FreeBSD host
#pass in on em0 proto icmp from 192.168.100.0/24 to (em0) keep state

# allow ICMP from Home network to Office network
pass in on em0 proto icmp from 192.168.0.0/24 to 192.168.100.0/24 keep state

############################
### outbound traffic ###
############################

# allow all outbound traffic from FreeBSD
pass out keep state

Активні підключення в pftop:

Тут:

  • In 192.168.0.165:50286 => 192.168.0.2:22: SSH робочий ноут на FreeBSD
  • In 178.***.***.236:56432 => 192.168.0.2:51820: підключення з дому через NAT Port-forwarding на офісному роутері до VPN на FreeBSD
  • In 10.8.0.3:39442 => 192.168.0.165:22: SSH з дому на робочий ноут
  • Out 10.8.0.1:50589 => 10.8.0.3:22: SSH з FreeBSD на домашній

P.S. Який це дикий кайф – оцей во “traditional networking” а не ці всі AWS VPC і його subnets…

Loading