Там часто всякі боти запускаються, нічого незвичного.
Далі, вирішив з 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).
А друге – сама причина 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.
Це при тому, що зазвичай переглядів кілька десятків, ну максимум 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 зник.
Наступний крок в процесі налаштування домашнього NAS на FreeBSD – додати NFS share.
Samba зробили в попередній частині – тепер до неї додамо шари з NFS: моя ідея в тому, щоб Samba share використовувалась для всяких медіа-ресурсів, до яких потрібен доступ з телефонів та TV, а NFS буде виключно для Linux-хостів – двох ноутбуків в різних мережах (дім і офіс), які на цей розділ будуть робити свої бекапи з rsync, rclone або restic.
Всі частини серії по налаштуванню домашнього NAS на FreeBSD:
В 192.168.0.2:/backups каталог /backups вказуємо від корня, який задали в /etc/exports: тобто корінь у нас “/nas/nfs” – значить на клієнтах він буде “/“, і, відповідно, внутрішні датасети монтуємо від цього корня як /backups.
А по-дефолту 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
Тепер на клієнті від звичайного юзера у нас доступу все ще нема:
Допомогли на форумі FreeBSD, див. NFSv4 and share for multiply networks (взагалі, FreeBSD community дуже відкрите, і набагато менш токсичне, аніж ті ж форуми Arch Linux).
І, звісно, хочеться доступ до 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 начебто розширили синтаксис, і там вже можна передати список, розділений “;“:
[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“:
Дуже рідко бувають проблеми з апгрейдами на Arch Linux, а така ситуація, як сьогодні, в мене за майже 10 років користування системою вперше.
Отже, що трапилось:
встановив апгрейди з pacman -Syu
після установки sudo reboot зависав
ребутнутись вдалось тільки з sudo reboot -f (force)
після ребуту – X.Org запустився, SDDM теж, Plasma теж – але на робочому столі все зависало і не реагувало на клаву і мишку
Дичина якась 🙂
Пофіксити вдалось, хоча так і не зрозумів що саме і чому впало.
Проблема #1: 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 працював, і я зміг підключатись з іншого ноутбука аби подебажити і пофіксити.
Хоча 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-оточенні:
Коротше – класичний приклад неочевидної “магії” systemd, D-Bus та системних hooks під час апгрейду.
Lessons learned
якщо під час pacman -Syu з’являються помилки systemd/dbus, їх не варто ігнорувати – а я звик, що апгрейди проходять без проблем, і не подивився на pacman output в консолі, а відразу почав ребутати машину
зависання GUI не завжди означає проблему в Destop Environment або Wayland/X11
повторний запуск системних hooks у стабільному середовищі може повністю виправити систему
Прийшов час трохи привести в порядок SSH на самому FreeBSD та на клієнтах – ноутбуках з Arch Linux, бо на домашніх машинках досі використовую парольну аутентифікацію.
Власне, описані нижче налаштування не специфічні ні для FreeBSD, ні для Linux, бо SSH server один і той самий на всіх системах (OpenSSH_9.9p2 на FreeBSD 14.3 і OpenSSH_10.2p1 на Arch Linux).
хост з FreeBSD/NAS: доступний тільки в локальній мережі і VPN, тому SSH brute force не очікується (але при параної або на публічно доступних серверах можна додати Fail2Ban чи SSHGuard)
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 # 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
Виконуємо 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:
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, якщо все добре – то команда просто нічого не виведе:
...
# 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” – то система все одно може запросити парольну аутентифікацію
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.
Продовжуємо налаштовувати домашній NAS на FreeBSD.
Власне, NAS – Network System, і хочеться мати до нього доступ з інших девайсів – з Linux та Windows хостів, з телефонів, телевізорів.
Тут у нас на вибір дві основні опції – Samba та NFS. Можна, звісно, згадати і sshfs – але це рішення точно не для домашньої мережі (хоча простіше).
Я для себе вирішив для Windows (на ігровому ПК), Android телефонів та Android TV зробити доступ з Samba share – а NFS буде чисто для Linux систем для бекапів.
В цьому пості налаштуємо Samba на FreeBSD, а в наступному – додамо NFS.
Всі частини серії по налаштуванню домашнього NAS на FreeBSD:
без можливості логіна в систему – /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)
І права доступу до каталога тепер – можна глянути розширену інформацію з getfacl, аби отримати POSIX/NFSv4 ACL, які ZFS використовує замість класичних Unix-бітів:
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.
Або більше детально, просто явно вказуємо деякі дефолтні опції:
[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 # 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
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 з серверу:
Через файл mnt-nas\x2dshared.automount systemd відстежує доступ до /mnt/nas-shared, і як тільки ми виконаємо якусь дію (наприклад, cd /mnt/nas-shared) – systemd виконає mnt-nas\x2dshared.mount, який власне підключить розділ.
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, вводимо логін-пароль:
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“:
Для подальшої роботи з дисками створимо постійні GPT lables – іменовані ідентифікатори розділів, які зберігаються в GPT-таблиці розділів і читаються при старті системи.
Вони не змінюються після reboot, не залежать від порядку SATA портів, не залежать від того, як ядро виявило диск.
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)
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.
включити шифрування можна тільки створенні 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 # 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 підтримують задання квот на розмір датасетів – тобто, максимальний розмір, який він може займати.
Зручно, наприклад, аби якийсь 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 дозволяє зарезервувати місце для датасету, гарантуючи доступний простір незалежно від заповненості пулу.
Важливо, що 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, які дозволяють швидко відкотитись до попереднього стану.
ZFS працює за принципом COW (Copy On Write), тобто при зміні в блоках даних – зміни робляться в новому блоку, а старі блоки не перезаписуються, поки на них є активні посилання
при створенні снапшоту ZFS не копіює дані, а створює таке посилання на цей блок
далі, коли ми робимо зміни в даних датасета, для якого є снапшот – то зміни на диску робляться в нових блоках даних, а доступ до старих зберігається через снапшот
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 # 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)
Аби відновити зі снапшоту – можна або просто скопіювати з каталога /nas/data/.zfs/snapshot/test-snap/ з cp, або, якщо треба відкотити весь датасет, то використати zfs rollback – але в такому разі всі зміни, які були зроблені після створення снапшоту будуть втрачені:
Ну і ZFS Boot Environments працюють через ті самі снапшоти – під час виконання freebsd-update install автоматично створюється копія даних, на яку можна відкотитись в разі проблем.
Взагалі ZFS Boot Environments дуже цікава штука, може, окремо про неї напишу.
Замість повного rollback – аби не перезаписувати дані на поточному датасеті – можна зробити клонування снапшоту в новий датасет:
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 задач, для яких в /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 в автостарт:
І налаштовуємо 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"
&
Під час 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 можна вказати, які саме пули перевіряти.
З 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 # 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.
Але зараз, аби підключитись до якогось хосту в мережах треба вказувати IP-адресу.
Можна, звісно, прописувати все в файлах /etc/hosts, але це і не дуже зручно, і будуть клієнти типу Android-телефонів, і взагалі – хочеться все красівоє.
Тому зробимо локальний DNS, на якому буде централізований DNS для всієї інфраструктури:
власну локальну DNS-зону .setevoy
DNS-відповіді для .setevoy, що залежать від мережі, з якої приходить запит (office/home/VPN)
резолвінг зовнішніх доменів через forward-DNS (Cloudflare/Google)
Всі частини серії по налаштуванню домашнього NAS на FreeBSD:
Я колись мав 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.
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
...
Тепер при підключенні 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, і відразу поверне відповідь клієнту.
root@setevoy-nas:/home/setevoy # unbound-checkconf && service unbound reload
Тепер Unbound в порядку пріорітету спочатку перевірить свою локальну зону, якщо google.com в нього на обслуговуванні нема – то він переадресує запит до Cloudflare або Google.
Налаштування логів
Робив для того, аби подивись як впливає налаштування forward-zone – тому теж нехай тут буде.
Тепер відключаємо 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:
Основна ідея – поєднати (нарешті!) мій “офіс” і квартиру, а пізніше, можливо, ще і підключити сервер, на якому зараз працює 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-сервісом.
“офіс”: окрема локальна мережа 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: основна “робоча” частина завантажується як модуль ядра, а для роботи з ним встановлюється окремий пакет.
роутити інтернет через 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 клієнт переконується, що підключився саме до того сервера, публічний ключ якого він знає
а далі, з цими ключами, відбувається і шифрування даних
Слово “сервер” все ж беру в лапки, бо, як було зазначено вище – 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 # 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.
Блок 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 все аналогічно – в ядрі є модулі, нам треба тільки встановити пакет з утилітами.
Тут в 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 тепер буде таким:
В принципі, на цьому вже майже все готове – доступ є, все працює.
Але чого хочеться ще – це мати прямий доступ з домашнього ноута на робочий і з робочого на домашній, бо на робочому ноуті 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 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
##################
### 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…
Зараз в системі є три “штатних” фаєрволи – Packet Filter (PF), IP Firewall (IPFW) та IP Filter (IPF):
pf: зараз фактично дефолтна опція, був портований до FreeBSD з OpenBSD
ipfw: “історичний” фаєрвол ля FreeBSD, був доданий ще в 90-х роках
ipf: open source фаєрвол, портований в багато різних систем, в т.ч. OpenBSD та FreeBSD
Фаєрволи діляться на кілька типів: “inclusive” або “exclusive”, та “stateful” або “stateless”:
inclusive/exclusive: поведінка за замовчуванням – або дозволяти трафік, якщо для нього немає явного правила block (inclusive), або блокувати весь трафік, якщо для нього немає явного правила allow (exclusive)
stateful/stateless: чи зберігає firewall стан підключень, у stateful-режимі відповідний вхідний трафік для вже встановленого зʼєднання дозволяється автоматично, а у stateless-режимі firewall не зберігає стан і перевіряє кожен пакет окремо за правилами
Давайте спробуємо pf – потикав його, виглядає доволі простим, має всі потрібні можливості і приємний синтаксис.
Для управління pf є окрема CLI утиліта pfctl, для логування запитів – pflog, а для перевірки того, що зараз відбувається у pf – утиліта pftop.
Всі частини серії по налаштуванню домашнього NAS на FreeBSD:
quick: корисна штука – чи перевіряти правила далі, чи, якщо пакет підпадає під поточне правило, зупинити обробку і не перевіряти решту правил (далі буде приклад)
interface: імʼя інтерфейсу або групи, до яких застосовується правило (якщо не вказано – правило діє на всі інтерфейси)
Важливо: pf працює за принципом “last match wins” – firewall перевіряє всі правила від першого до останнього, і вирішальним є останнє правило, під яке підпадає пакет.
В ipfw, навпаки – важливий номер правила, і спрацьовує перше правило, умови якого відповідають пакету (“first match wins”). В ipf (ipfilter) використовується така ж модель – правила також обробляються за принципом first match wins.
Далі буде приклад того, як порядок правил впливає на доступи.
З коробки в системі є набір прикладів:
root@setevoy-nas:/home/setevoy # ls -l /usr/share/examples/pf/
total 44
-r--r--r-- 1 root wheel 1233 Jun 6 2025 ackpri
-r--r--r-- 1 root wheel 925 Jun 6 2025 faq-example1
-r--r--r-- 1 root wheel 3129 Jun 6 2025 faq-example2
-r--r--r-- 1 root wheel 4711 Jun 6 2025 faq-example3
-r--r--r-- 1 root wheel 1062 Jun 6 2025 pf.conf
-r--r--r-- 1 root wheel 848 Jun 6 2025 queue1
-r--r--r-- 1 root wheel 1110 Jun 6 2025 queue2
-r--r--r-- 1 root wheel 480 Jun 6 2025 queue3
-r--r--r-- 1 root wheel 900 Jun 6 2025 queue4
-r--r--r-- 1 root wheel 256 Jun 6 2025 spamd
Додамо для перевірки просте правило – блокувати всі підключення окрім SSH з мого робочого ноутбуку.
Знаходимо IP ноутбука:
[setevoy@setevoy-work ~] $ ip a s wlan0
5: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
...
inet 192.168.0.165/24 brd 192.168.0.255 scope global dynamic noprefixroute wlan0
...
inet 192.168.0.164/24 brd 192.168.0.255 scope global secondary dynamic noprefixroute wlan0
...
Додаємо правила в /etc/pf.conf:
# skip loopback traffic
set skip on lo
# default deny
block all
# allow SSH only from specific hosts
pass in proto tcp from { 192.168.0.165, 192.168.0.164 } to any port 22 keep state
# allow all outgoing traffic
pass out all keep state
Тут ми:
ігноруємо трафік на loopback interface
блокуємо все
дозволяємо вхідний трафік на порт 22 (SSH) з двох адрес – 192.168.0.165 і 192.168.0.164
дозволяємо весь outgoing трафік
Замість port 22 можна вказати ім’я “ssh” – тоді pf перевірить список імен і портів у файлі /etc/services:
Аби перевірити синтаксис /etc/pf.conf, але не застосовувати правила – виконуємо pfctl -vnf:
-v: verbose (можна два v для ще більшої деталізації)
-n: no apply, тільки перевірка синтаксису
-f: ім’я файлу для перевірки
root@setevoy-nas:/home/setevoy # pfctl -vnf /etc/pf.conf
set skip on { lo }
block drop all
pass in inet proto tcp from 192.168.0.165 to any port = ssh flags S/SA keep state
pass in inet proto tcp from 192.168.0.164 to any port = ssh flags S/SA keep state
pass out all flags S/SA keep state
Тут звертаємо увагу на дві цікаві речі:
flags S/SA: pf застосовує правило лише до початку TCP-зʼєднання (SYN), щоб створити state, і подальші пакети цього підключення обробляються вже через state table, а не через повну перевірку правил
keep state: це як раз про stateful/stateless – pf зберігає стан зʼєднання в state table, і якщо пакет належить до вже встановленого підключення, він пропускається без повторної перевірки всього ruleset
Хоча ми не задавали TCP-флаги явно в /etc/pf.conf, pf автоматично додає flags S/SA до stateful TCP-правил для коректної обробки нових зʼєднань.
Тестовий запуск і SSH
Важливий момент: якщо робимо по SSH, то при запуску pf підключення буде розірване, а якщо помилились в правилах – то не зможемо підключитись.
Запускаємо pf на 2 хвилини, перевіряємо, чи працює SSH.
Якщо робили по SSH – підключення буде розірване (бо не було SYN/SYN-ACK на момент роботи pf), перепідключаємось по SSH ще раз.
І, якщо все ОК, то вже стартуємо сам сервіс pf та pflog для логування:
root@setevoy-nas:/home/setevoy # service pflog start
Starting pflog.
root@setevoy-nas:/home/setevoy # service pf start
Enabling pf
Головне правило при налаштуванні pf по SSH – не виконувати start/restart, бо підключення буде розірване.
Натомість якщо треба змінити конфіг – то використовуємо спочатку pfctl -n для перевірки синтаксису, а потім pfctl -f /etc/pf.conf або service pf reload.
Можемо глянути з nmap з робочої машинки як наш сервер виглядає “ззовні”:
[setevoy@setevoy-work ~] $ nmap -Pn -p 1-1024 192.168.0.181
...
Not shown: 1023 filtered tcp ports (no-response)
PORT STATE SERVICE
22/tcp open ssh
Nmap done: 1 IP address (1 host up) scanned in 7.02 seconds
Власне, тільки 22 порт і бачимо, і то тільки тому, що nmap запускався з хоста, якому дозволений SSH.
Packet Filter Rules Ordering
Простий приклад того, як порядок правил впливає на те, чи буде пакет пропущений до системи, тобто підхід “last match wins”.
Поки закоментуємо block all, робимо pfctl -f /etc/pf.conf або service pf reload, і запускаємо “сервер” з NetCat, який слухає порт 10000:
root@setevoy-nas:/home/setevoy # nc -v -l 10000
Тепер з іншою машини відправляємо текст:
[setevoy@setevoy-work ~] $ echo Test | nc 192.168.0.2 10000
192.168.0.2 10000 (ndmp) open
І він приходить на “сервер” – все працює:
root@setevoy-nas:/home/setevoy # nc -v -l 10000
Connection from 192.168.0.165 58018 received!
Test
Тепер робимо тест: спочатку додаємо pass port 10000передblock all:
...
# this will not work
pass in proto tcp to any port 10000 keep state
# default deny
block all
...
Робимо reload:
root@setevoy-nas:/home/setevoy # pfctl -vnf /etc/pf.conf && service pf reload
Запускаємо на сервері nc -v -l 10000, пробуємо на клієнті ще раз, з -w вказуємо таймаут для підключення, отримуємо помилку “Connection timed out”:
$ echo Test | nc -v -w 3 192.168.0.2 10000
192.168.0.2 10000 (ndmp): Connection timed out
А тепер переносимо pass після block:
...
# default deny
block all
# this will work
pass in proto tcp to any port 10000 keep state
...
Ще раз service pf reload, ще раз запускаємо nc -v -l 10000, і тепер з клієнта:
$ echo Test | nc -v -w 3 192.168.0.2 10000
192.168.0.2 10000 (ndmp) open
Знов маємо “Test” на сервері.
Packet Filter та quick в rules
Про quick вже згадував в Rules Syntax, давайте глянемо, як це працює на ділі.
Повертаємо pass 10000 перед block:
...
# allow nc demo
pass in proto tcp to any port 10000 keep state
block all
...
Робимо reload, перевіряємо з nc – знов отримуємо “Connection timed out”.
А тепер, не змінюючи порядок – додамо до правила з pass in 10000 ключ quick:
...
# allow nc demo
pass in quick proto tcp to any port 10000 keep state
block all
...
Тепер підключення працює, бо запит попав під правило з pass і pf не перевіряє правила далі, а просто пропускає пакет.
Packet Filter logging з pflog
Запуск pflog ми вже додали в /etc/rc.conf і зробили service pflog start, але зараз він нічого не пише.
Аби включити запис логів для правила – додаємо його після action і, якщо є, то direction, наприклад:
...
# default deny
block log all
...
Перевіряємо конфіг, застосовуємо зміни:
root@setevoy-nas:/home/setevoy # pfctl -nf /etc/pf.conf
root@setevoy-nas:/home/setevoy # service pf reload
pflog для логування створює окремий інтерфейс – pflog0 (ім’я можна перевизначити з pflog_flags):
Тому читаємо його не cat/less, а теж tcpdump – тільки замість -i (interface) вказуємо -r і файл (“read from a saved packet file rather than to read packets from a network interface“):
root@setevoy-nas:/home/setevoy # tcpdump -n -e -ttt -r /var/log/pflog | head
reading from file /var/log/pflog, link-type PFLOG (OpenBSD pflog file), snapshot length 116
00:00:00.000000 rule 0/0(match): block in on em0: 192.168.0.165.17500 > 255.255.255.255.17500: UDP, length 131
00:00:00.000100 rule 0/0(match): block in on em0: 192.168.0.165.17500 > 192.168.0.255.17500: UDP, length 131
...
Логи для SSH only
Зараз у нас логуються всі заблоковані запити, і їх може бути дуже багато.
Якщо хочемо записувати події тільки по SSH – то прибираємо log з block all і додаємо нове правило з block log port 22:
...
# default deny
block all
# log and block SSH from everyone else
block log proto tcp to any port 22
# allow SSH only from specific hosts
pass in proto tcp from { 192.168.0.165, 192.168.0.164 } to any port 22 keep state
...
Тепер у нас блокується все по дефолту, але без записів в лог, і блокується SSH для всіх окрім 192.168.0.165, 192.168.0.164, але подія зберігається в лог:
Якщо хочемо логувати ще і всі нові дозволені підключення – просто додаємо log до pass in 22:
...
# allow SSH only from specific hosts
pass in log proto tcp from { 192.168.0.165, 192.168.0.164 } to any port 22 keep state
...
І отримуємо запис з “pass” в лог-файлі:
...
00:00:00.000000 rule 2/0(match): pass in on em0: 192.168.0.165.60218 > 192.168.0.2.22: [...]
...
Packet Filter та макроси
Macros, по суті, це як змінні в програмуванні. Точніше кажучи, це аналог const – вони підставляються під час завантаження конфігурації і не змінюється під час роботи pf.
Наприклад, можемо створити список портів і IP-адрес, для яких дозволений доступ:
І далі використовуємо їх в правилах як $ssh_clients та $allowed_tcp_ports:
# allowed tcp ports
allowed_tcp_ports = "{ 22, 10000 }"
# allowed ssh clients
ssh_clients = "{ 192.168.0.4, 192.168.0.165, 192.168.0.164 }"
# skip loopback traffic
set skip on lo
# default deny
block all
# restrict ssh source addresses
pass in proto tcp from $ssh_clients to any port $allowed_tcp_ports keep state
# allow all outgoing traffic
pass out all keep state
Тепер, якщо нам треба додати порт або адресу – не треба шукати їх по всьому файлі, а просто міняємо значення в одному місці, де “оголошується” макрос.
Packet Filter та Tables
Ідея використання Tables аналогічна до макросів – ми створюємо “змінну”, яка містить набір значень (IP-адрес або мереж), які будуть використовуватись в правилах.
Але якщо макроси використовуються для спрощення роботи з конфігураційним файлом – то таблиці призначені для великих списків, бо пошук по них відбувається дуже швидко.
Крім того, таблиці можемо міняти під час роботи pf без необхідності робити service pf reload для застосування змін.
Виносимо список адрес в таблицю з іменем ssh_clients:
І потім використовуємо в правилах – тільки тепер замість $ssh_clients вказуємо як <ssh_clients>:
# macros
allowed_tcp_ports = "{ 22, 10000 }"
# ssh allowed clients
#ssh_clients = "{ 192.168.0.4, 192.168.0.165, 192.168.0.164 }"
table <ssh_clients> { 192.168.0.4, 192.168.0.165, 192.168.0.164 }
set skip on lo
# allow nc demo
pass in quick proto tcp to any port 10000 keep state
block all
# allow ssh only from specific hosts
pass in proto tcp from <ssh_clients> to any port 22 keep state
...
Тепер у нас все ще є доступ з робочого ноутбуку, але не буде з хоста 192.168.0.86:
...
00:00:01.012829 rule 1/0(match): block in on em0: 192.168.0.86.50451 > 192.168.0.2.22: [...]
...
Tables в окремих файлах
Якщо списки великі – їх можна винести в додаткові файли, які потім підключають через /etc/pf.conf, наприклад – робимо /etc/pf_clients.conf з тими самими адресами:
А у /etc/pf.conf замість “table <ssh_clients> { 192.168.0.4, 192.168.0.165, 192.168.0.164 }” вказуємо таблицю з persist file:
...
# ssh allowed clients
table <ssh_clients> persist file "/etc/pf_clients.conf"
...
# allow ssh only from specific hosts
pass in proto tcp from <ssh_clients> to any port 22 keep state
...
pfctl та зміни в Tables
Перевіряємо таблицю зараз:
root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T show
192.168.0.0/24
!192.168.0.86
Тепер, якщо хочемо видалити !192.168.0.86 – виконуємо:
Якщо хочемо відновити правила, які є у файлі /etc/pf_clients.conf – робимо або service pf reload, або pfctl replace і файл, з якого треба взяти правила:
Anchors дозволяють описувати правила в окремих файлах і підключати їх до основного /etc/pf.conf. Це схоже на використання окремих файлів для tables (наприклад, /etc/pf_clients.conf), але замість списків адрес anchors містять повноцінні rulesets.
Крім того, anchors можуть динамічно завантажуватись і змінюватись за допомогою pfctl під час роботи pf.
Створюємо іменований anchor:
...
block all
# allow ssh only from specific hosts
pass in proto tcp from <ssh_clients> to any port 22 keep state
# new dynamic anchor here
anchor "ssh"
# allow all outgoing traffic
pass out all keep state
Це – динамічний anchor, бо для нього не вказаний файл з правилами, і правила в ньому при service pf restart будуть видалені, але при service pf reload залишаться.
Можемо додати правило прямо “на льоту”:
root@setevoy-nas:/home/setevoy # echo 'pass in proto tcp from <ssh_clients> to any port 22 keep state' | pfctl -a ssh -f -
Перевіряємо:
root@setevoy-nas:/home/setevoy # pfctl -a ssh -s rules
pass in proto tcp from <ssh_clients> to any port = ssh flags S/SA keep state
Або створюємо файл, і виконуємо pfctl -a ssh -f /path/to/anchor.conf.
Інший варіант – заповнювати anchors під час reload чи restart, вказавши файл прямо до /etc/pf.conf в самому anchor.
Наприклад, додаємо нове правило:
root@setevoy-nas:/home/setevoy # cat /etc/pf.anchors/ssh
pass in proto tcp from 10.0.0.100 to any port 22 keep state
І описуємо його load у /etc/pf.conf:
...
anchor "ssh"
load anchor "ssh" from "/etc/pf.anchors/ssh"
# allow all outgoing traffic
pass out all keep state
Перевіряємо конфіг:
root@setevoy-nas:/home/setevoy # pfctl -vnf /etc/pf.conf
...
pass out all flags S/SA keep state
Loading anchor ssh from /etc/pf.anchors/ssh
pass in inet proto tcp from 10.0.0.100 to any port = ssh flags S/SA keep state
Виконуємо reload:
root@setevoy-nas:/home/setevoy # service pf reload
Reloading pf rules.
І ще раз дивимось правила в anchor "ssh":
root@setevoy-nas:/home/setevoy # pfctl -a ssh -s rules
pass in inet proto tcp from 10.0.0.100 to any port = ssh flags S/SA keep state
Ну і на цьому, мабуть, все, хоча можливостей у pf ще багато – див. посилання нижче.
Установку будемо робити по SSH з bsdinstall – завантажимо систему в режимі LiveCD, включимо SSH, і далі вже з робочого ноута виконаємо установку системи.
На віртуальній машині є три диски – аналогічно тому, як це буде на ThinkCentre:
Вибираємо Live System:
Логінимось з root:
Піднімаємо мережу:
# ifconfig em0 up
# dhclient em0
Налаштування SSH на FreeBSD LiveCD
Для SSH нам треба буде задати пароль root і робити зміни в /etc/ssh/sshd_config, але зараз це все не працює, бо система змонтована в read-only:
Дивимось, які розділи є зараз:
І робимо “dirty hack:
монтуємо нову файлову систему tmpfs в RAM на /mnt
копіюємо туди вміст /etc з LiveCD
монтуємо tmpfs поверх /etc (перекриваючи read-only каталог з ISO)
копіюємо підготовлені файли з /mnt назад у новий /etc
Виконуємо:
# mount -t tmpfs tmpfs /mnt
# cp -a /etc/* /mnt/
# mount -t tmpfs tmpfs /etc
# cp -a /mnt/* /etc/
Синтаксис mount для tmptfs – mount -t <fstype> <source> <mountpoint>, в source значення обов’язкове, тому ще раз вказуємо tmpfs.
Тепер задаємо пароль з passwd і стартуємо sshd з onestart:
# passwd
# service sshd onestart
Але SSH все ще не пустить, бо по дефолту логін root заборонений:
Вибираємо що будемо додавати в систему – ports треба, src опціонально, на реальному NAS точно варте:
Disk partitioning
Будемо робити мінімальну розбивку диска, тому вибираємо Manual:
Встановлювати систему будемо на ada0, вибираємо його і Create:
Далі треба вибрати схему, тут, в принципів, стандартно для 2025 року – GPT:
Підтверджуємо зміни, і маємо нову partition table на системному ada0:
Розділ freebsd-boot
Далі треба створити самі розділи.
Ще раз вибираємо ada0, Create, і створюємо розділ під freebsd-boot.
Це тільки зараз на віртуальній машині, на самому ThinkCentre тут будемо робити тип efi з розміром мегабайт у 200-500.
Зараз задаємо:
Type: freebsd-boot
Size: 512K
Mountpoint: empty
Label: empty
Підтверджуємо, йдемо до наступного розділу.
Розділ freebsd-swap
Ще раз Create, додаємо Swap.
Враховуючи що у нас на ThninkCentre будуть:
8 – 16 GB RAM
без sleep/hibernate
буде UFS і ZFS
То 2 гігабайти вистачить.
Задаємо:
Type: freebsd-swap
Size: 2GB
Mountpoint: empty
Label: empty
Розділ root з UFS
Основна система буде на UFS, бо вона дуже стабільна, не потребує для роботи RAM, швидко монтується, проста у відновленні, без складних механізмів кешування.
Задаємо:
Type: freebsd-ufs
Size: 14GB
Mountpoint: /
Label: rootfs – просто ім’я для нас
Решту дисків налаштуємо пізніше, зараз вибираємо Finish і Commit:
Finihsing installation
Чекаємо завершення копіювання:
Налаштовуємо мережу:
Таймзону:
В System Configuration – sshd, без мишки, включаємо ntpd і powerd:
System Hardening – враховуючи що це буде домашній NAS, але буду, мабуть, відкривати доступ ззовні, хоч і за фаєрволом – то є сенс трохи затюнити security:
read_msgbuf: дозволяємо dmesg тільки root
proc_debug: дозволяємо ptrace тільки root
random_pid: номера PID в більш рандомному порядку
clear_tmp: чистимо /tmp при ребутах
secure_console: вимагаємо пароль root при логіні з фізичної консолі
Вся “магія” ZFS – що в ньому все йде “з коробки” – не треба окремий LVM і його групи, не треба mdadm для RAID.
Для роботи з дисками в ZFS основна утиліта – zpool, для роботи з даними (datasets, файловими системами, снапшотами) – zfs.
Для об’єднання одного або кількох дисків у єдиний логічний storage ZFS використовує pool – в Linux LVM аналог volume group.
Створюємо пул:
root@test-nas-1:/home/setevoy # zpool create tank mirror ada1p1 ada2p1
Тут tank – ім’я пула, в mirror вказуємо, що це буде RAID1, і передаємо список розділів, які в цей пул включаються.
Перевіряємо:
root@test-nas-1:/home/setevoy # zpool status
pool: tank
state: ONLINE
config:
NAME STATE READ WRITE CKSUM
tank ONLINE 0 0 0
mirror-0 ONLINE 0 0 0
ada1p1 ONLINE 0 0 0
ada2p1 ONLINE 0 0 0
errors: No known data errors
ZFS відразу монтує цей пул в /tank:
root@test-nas-1:/home/setevoy # mount
/dev/ada0p3 on / (ufs, local, soft-updates, journaled soft-updates)
devfs on /dev (devfs)
tank on /tank (zfs, local, nfsv4acls)
Якщо хочемо змінити маунтпоінт – виконуємо zfs set mountpoint:
root@test-nas-1:/home/setevoy # zfs set mountpoint=/data tank
І він відразу монтується в новий каталог:
root@test-nas-1:/home/setevoy # mount
/dev/ada0p3 on / (ufs, local, soft-updates, journaled soft-updates)
devfs on /dev (devfs)
tank on /data (zfs, local, nfsv4acls)