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.

Попередні пости цієї серії:

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, шифруванням, снапшотами, моніторингом.

Попередні пости цієї серії:

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

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

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)

Вибір 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“.

Архітектура мережі

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

  • “офіс”: окрема локальна мережа 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

FreeBSD: Home NAS, part 2 – знайомство з Packet Filter (PF) firewall
0 (0)

17 Грудня 2025

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

Колись я працював з IPFW – FreeBSD: начальная настройка IPFW, 2012 рік.

Зараз в системі є три “штатних” фаєрволи – 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.

Всі частини цієї серії:

Початок роботи з Packet Filter (PF)

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

Додаємо pf_enable=yes та pflog_enable=yes до /etc/rc.conf для запуску pf та pflog при рестарті системи:

root@setevoy-nas:/home/setevoy # sysrc pf_enable=yes
pf_enable: NO -> yes

root@setevoy-nas:/home/setevoy # sysrc pflog_enable=yes
pflog_enable: NO -> yes

Але поки не запускаємо, бо pf буде шукати дефолтний файл /etc/pf.conf, якого ще нема.

Packet Filter Rules Syntax

Детально є в документації OpenBSD, PF – Packet Filtering, зараз коротко глянемо основне:

action [direction] [log] [quick] [on interface] [af] [proto protocol]
       [from src_addr [port src_port]] [to dst_addr [port dst_port]]
       [flags tcp_flags] [state]

Тут:

  • action: pass або block
  • direction: in або out
  • log: чи записувати подію в лог-файл
  • quick: корисна штука – чи перевіряти правила далі, чи, якщо пакет підпадає під поточне правило, зупинити обробку і не перевіряти решту правил (далі буде приклад)
  • interface: імʼя інтерфейсу або групи, до яких застосовується правило (якщо не вказано – правило діє на всі інтерфейси)
  • protocol: TCP або UDP, чи інший із /etc/protocols
  • src_addr та dst_addr: можна вказати таблицю або макрос (про них теж далі), повне доменне ім’я, ім’я інтерфейсу чи групи або any
  • src_port та dst_port: аналогічно – порти, номер(и), або ім’я з файлу /etc/services
  • tcp_flags: фаги TCP, які мають бути в заголовках пакета, аби правило було застосоване
  • state:

Важливо: 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:

$ cat /etc/services | grep ssh
ssh                22/tcp
ssh                22/udp

Аби перевірити синтаксис /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 підключення буде розірване, а якщо помилились в правилах – то не зможемо підключитись.

Тому є такий лайфхак:

root@setevoy-nas:/home/setevoy # sleep 120; pfctl -d

Запускаємо 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):

root@setevoy-nas:/home/setevoy # ifconfig pflog0
pflog0: flags=1000141<UP,RUNNING,PROMISC,LOWER_UP> metric 0 mtu 33152
        options=0
        groups: pflog

Пробуємо підключитись з якогось “лівого” хосту, на який нема правила pass in:

setevoy@test-nas-1:~ $ ssh 192.168.0.181

Перевірити логи можемо наживо прямо з інтерфейсу:

root@setevoy-nas:/home/setevoy # tcpdump -n -e -ttt -i pflog0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on pflog0, link-type PFLOG (OpenBSD pflog file), snapshot length 262144 bytes
 00:00:00.000000 rule 0/0(match): block in on em0: 192.168.0.113.44161 > 255.255.255.255.6667: UDP, length 172
 00:00:02.301893 rule 0/0(match): block in on em0: 192.168.0.86.19569 > 192.168.0.181.22: Flags [S], seq 982711563, win 65535, options [mss 1460,nop,wscale 6,sackOK,TS val 370325371 ecr 0], length 0
 00:00:01.026055 rule 0/0(match): block in on em0: 192.168.0.86.19569 > 192.168.0.181.22: Flags [S], seq 982711563, win 65535, options [mss 1460,nop,wscale 6,sackOK,TS val 370326401 ecr 0], length 0

По дефолту pflog пише в файл /var/log/pflog, і пише не в text, а в pcap:

root@setevoy-nas:/home/setevoy # file /var/log/pflog
/var/log/pflog: pcap capture file, microsecond ts (little-endian) - version 2.4 (OpenBSD PFLOG, capture length 116)

Тому читаємо його не 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, але подія зберігається в лог:

root@setevoy-nas:/home/setevoy # tcpdump -n -e -ttt -i pflog0
...
 00:00:00.000000 rule 1/0(match): block in on em0: 192.168.0.86.27268 > 192.168.0.2.22: [...]
...

Якщо хочемо логувати ще і всі нові дозволені підключення – просто додаємо 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-адрес, для яких дозволений доступ:

# 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 }"

І далі використовуємо їх в правилах як $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 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 }
...

І потім використовуємо в правилах – тільки тепер замість $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

...

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

root@setevoy-nas:/home/setevoy # pfctl -vnf /etc/pf.conf 
allowed_tcp_ports = "{ 22, 10000 }"
table <ssh_clients> { 192.168.0.4 192.168.0.165 192.168.0.164 }
...

Робимо reload, і підключення працює.

Також можна вказувати негативне значення з ! для виключення якоїсь адреси.

Наприклад, дозволяємо SSH з усієї мережі 192.168.0.0/24, але забороняємо з 192.168.0.86:

...
table <ssh_clients> { 192.168.0.0/24, !192.168.0.86 }
...

Тепер у нас все ще є доступ з робочого ноутбуку, але не буде з хоста 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 з тими самими адресами:

root@setevoy-nas:/home/setevoy # cat /etc/pf_clients.conf
192.168.0.0/24
!192.168.0.86

А у /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 – виконуємо:

root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T delete !192.168.0.86
1/1 addresses deleted.

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

root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T show
   192.168.0.0/24

І SSH з 192.168.0.86 працює.

Додати адресу – аналогічно, тільки add замість delete:

root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T add !192.168.0.100
1/1 addresses added.
root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T show
   192.168.0.0/24
  !192.168.0.100

Можна навіть повністю очистити таблицю вручну з flush – поточна сесія SSH залишиться активною, але нові підключення працювати не будуть:

root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T flush
2 addresses deleted.
root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T show
root@setevoy-nas:/home/setevoy #

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

$ ssh -o ConnectTimeout=3 192.168.0.2
ssh: connect to host 192.168.0.2 port 22: Connection timed out

Збереження змін pfctl в Tables

Важливо, що зміни зроблені з pfctl зникнуть після reload чи рестарту, бо вони будуть тільки в пам’яті.

Аби зберігти їх – робимо зміни у /etc/pf.conf (або /etc/pf_clients.conf, якщо підключали файлом).

Наприклад, у нас в /etc/pf_clients.conf зараз є дві адреси:

root@setevoy-nas:/home/setevoy # cat /etc/pf_clients.conf
192.168.0.0/24
!192.168.0.86

І вони ж є у pf runtime:

root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T show
   192.168.0.0/24
  !192.168.0.86

Видаляємо !192.168.0.86 в рантаймі:

root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T delete !192.168.0.86
1/1 addresses deleted.

Якщо хочемо відновити правила, які є у файлі /etc/pf_clients.conf – робимо або service pf reload, або pfctl replace і файл, з якого треба взяти правила:

root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T replace -f /etc/pf_clients.conf
1 addresses added.

!192.168.0.86 знов на місці.

Якщо ж хочемо навпаки – зберігти зміни з пам’яті у файл, то виконуємо простий редірект з pfctl show > /etc/pf_clients.conf:

root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T delete !192.168.0.86
1/1 addresses deleted.

root@setevoy-nas:/home/setevoy # pfctl -t ssh_clients -T show > /etc/pf_clients.conf

root@setevoy-nas:/home/setevoy # cat /etc/pf_clients.conf
   192.168.0.0/24

Packet Filter та Anchors

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 ще багато – див. посилання нижче.

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

Loading

FreeBSD: Home NAS, part 1 – налаштування ZFS mirror (RAID1)
5 (1)

7 Грудня 2025

Є в мене ідея підняти собі вдома NAS на FreeBSD.

Для цього купив машинку Lenovo ThinkCentre M720s SFF – тиха, компактна, є можливість встановити 2 SATA III SSD + окремо M.2 слот під NVMe SSD.

Що планується:

  • на NVMe SSD: UFS і FreeBSD
  • на SATA SSD: ZFS з RAID1

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

Встановлювати будемо FreeBSD 14.3, хоча вже вийшла 15, але там є цікаві зміни, з нею окремо пограюсь.

Звісно, можна було взяти TrueNAS, яка базується на FreeBSD – але хочеться “ванільної” FreeBSD і все поробити ручками.

Всі частини цієї серії:

Установка FreeBSD через SSH

Установку будемо робити по 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 для tmptfsmount -t <fstype> <source> <mountpoint>, в source значення обов’язкове, тому ще раз вказуємо tmpfs.

Тепер задаємо пароль з passwd і стартуємо sshd з onestart:

# passwd
# service sshd onestart

Але SSH все ще не пустить, бо по дефолту логін root заборонений:

$ ssh [email protected]
([email protected]) Password for root@:
([email protected]) Password for root@:
([email protected]) Password for root@:

Задаємо PermitRootLogin yes в /etc/ssh/sshd_config, ще раз рестартимо sshd:

# echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
# service sshd onerestart

Тепер можемо залогінитись:

$ ssh [email protected]
([email protected]) Password for root@:
Last login: Sun Dec  7 12:19:25 2025
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!
...

root@:~ # 

Установка з bsdinstall

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

# bsdinstall

Вибираємо що будемо додавати в систему – 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 RAID

Логінимось вже під звичайним юзером:

$ ssh [email protected]
...
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7
Welcome to FreeBSD!
...
setevoy@test-nas-1:~ $ 

Встановлюємо vim 🙂

# pkg install vim

Перевіряємо що у нас з дисками.

З geom disk – інформація по фізичним девайсам, з gpart show – подивитись розділи на дисках.

Перевіряємо диски – три штуки:

root@test-nas-1:/home/setevoy # geom disk list
Geom name: ada0
Providers:
1. Name: ada0
   Mediasize: 17179869184 (16G)
   Sectorsize: 512
   Mode: r2w2e3
   descr: VBOX HARDDISK
   ident: VB262b53f7-adc5cd2c
   rotationrate: unknown
   fwsectors: 63
   fwheads: 16

Geom name: ada1
Providers:
1. Name: ada1
   Mediasize: 17179869184 (16G)
   Sectorsize: 512
   Mode: r0w0e0
   descr: VBOX HARDDISK
   ident: VB059f9d08-4b0e1f56
   rotationrate: unknown
   fwsectors: 63
   fwheads: 16

Geom name: ada2
Providers:
1. Name: ada2
   Mediasize: 17179869184 (16G)
   Sectorsize: 512
   Mode: r0w0e0
   descr: VBOX HARDDISK
   ident: VB3941028c-3ea0d485
   rotationrate: unknown
   fwsectors: 63
   fwheads: 16

І з gpart – поточний ada0, на який встановлювали систему:

root@test-nas-1:/home/setevoy # gpart show
=>      40  33554352  ada0  GPT  (16G)
        40      1024     1  freebsd-boot  (512K)
      1064   4194304     2  freebsd-swap  (2.0G)
   4195368  29359024     3  freebsd-ufs  (14G)

Диски ada1 та ada2 будуть під ZFS і його mirror (RAID1).

Якщо там щось було – все видаляємо:

root@test-nas-1:/home/setevoy # gpart destroy -F ada1
gpart: arg0 'ada1': Invalid argument
root@test-nas-1:/home/setevoy # gpart destroy -F ada2
gpart: arg0 'ada2': Invalid argument

Але так як це віртуалка, і диски пусті – то маємо “Invalid argument”, зараз це ОК.

Створюємо GPT розмітку на ada1 та ada2:

root@test-nas-1:/home/setevoy # gpart create -s gpt ada1
ada1 created
root@test-nas-1:/home/setevoy # gpart create -s gpt ada2
ada2 created

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

root@test-nas-1:/home/setevoy # gpart show ada1
=>      40  33554352  ada1  GPT  (16G)
        40  33554352        - free -  (16G)

Створюємо розділи під ZFS:

root@test-nas-1:/home/setevoy # gpart add -t freebsd-zfs ada1
ada1p1 added
root@test-nas-1:/home/setevoy # gpart add -t freebsd-zfs ada2
ada2p1 added

Ще раз перевіряємо:

root@test-nas-1:/home/setevoy # gpart show ada1
=>      40  33554352  ada1  GPT  (16G)
        40  33554352     1  freebsd-zfs  (16G)

Створення ZFS mirror із zpool

Вся “магія” 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)

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

root@test-nas-1:/home/setevoy # gpart show
=>      40  33554352  ada0  GPT  (16G)
        40      1024     1  freebsd-boot  (512K)
      1064   4194304     2  freebsd-swap  (2.0G)
   4195368  29359024     3  freebsd-ufs  (14G)

=>      40  33554352  ada1  GPT  (16G)
        40  33554352     1  freebsd-zfs  (16G)

=>      40  33554352  ada2  GPT  (16G)
        40  33554352     1  freebsd-zfs  (16G)

Якщо хочемо змінити маунтпоінт – виконуємо 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)

Налаштовуємо компресію даних – корисно для NAS, див. Compression і Compressing ZFS File Systems.

lz4 – дефолтний варіант зараз, включаємо її:

root@test-nas-1:/home/setevoy # zfs set compression=lz4 tank

Так як саму систему ми встановлювали з UFS, то для роботи ZFS треба додати пару параметрів в автозапуск.

Налаштування boot loader в /boot/loader.conf аби завантажити модулі ядра:

zfs_load="YES"

Або, щоб не лазити руками – sysrc і з -f ім’я файлу:

root@test-nas-1:/home/setevoy # sysrc -f /boot/loader.conf zfs_load="YES"

І до /etc/rc.conf, аби запустити демон zfsd і змонтувати файлові системи:

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

Ребутаємось, перевіряємо:

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

Все нам місці.

Тепер можна поробити всякий тюнінг – налаштувати окремі datasets, snapshots тощо.

Для Web UI можна буде спробувати Seafile чи FileBrowser.

Loading

VictoriaMetrics: Recording Rules для логів AWS Load Balancer
0 (0)

5 Грудня 2025

В продовження теми логів AWS Load Balancer: в попередньому пості  Golang: запис логів AWS Loab Balancer до VictoriaLogs зробили збір логів з власним logs collector на Golang, тепер треба з цих логів отримати щось корисне.

Раніше, коли у нас на проекті була Loki, ми з її RecordingRules створювали метрики, з яких потім малювали дашборди в Grafana і мали алерти з Alertmanager.

Тепер треба це перенести до VictoriaMetrics, у якої є власні RecordingRules, з якими VMAlert може виконувати запити до VictoriaLogs.

Колись про це писав у VictoriaLogs: створення Recording Rules з VMAlert, але там більше просто ознайомлення і налаштування – а сьогодні будемо робити конкретну задачу.

Що хочеться бачити в метриках з логів:

  • помилки 4хх
  • помилки 5хх
  • загальна кількість реквестів на ALB
  • час відповіді загальна
  • час target response time на конкретних ендпоінтах нашого Backend API

VictoriaLogs LogsQL ALB logs parsing

ALB access logs має чітко визначений формат полів (який, правда, іноді змінюється), див. документацію Access log entries.

Знаючи ці поля – з LogsQL pipes можемо їх розпарсити і створити потрібні fields, які потім будемо використовувати для обчислень.

extract pipe

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

Всі поля нам не потрібні, деякі поля створюються ще на етапі збору логів в Go (описаний у Створення Log parser), тому задаємо <_> на ті поля, які хочемо скіпнути, і тепер маємо таку конструкцію:

{stream="alb"}
| extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id> <_> <_> <_>"

На початку використовуємо Stream filter, який задається в нашому колекторі логів, а далі описуємо сам патерн полів.

Якщо поля в кінці точно ніколи не будуть потрібні – їх можна прибрати із запиту взагалі, тобто зробити просто так:

{stream="alb"}
| extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id>"

extract_regexp pipe

Нам потрібно буде робити час відповіді і помилки по конкретним ендпоінтам нашого Backend API, і нам вистачить першої частини URI.

Тобто зі строки “https://staging.api.challenge.example.co:443/roadmap/media/123 HTTP/2.0” треба отримати тільки “/roadmap”.

Сам запит йде в полі request_line:

Тут можемо використати extract_regexp pipe з яким отримуємо першу частину після “/” і зберігаємо результат в regex named capture group з ім’ям uri_path:

{stream="alb"}
| extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id>"
| extract_regexp `.*:443(?P<uri_path>/[^/?]+).* HTTP` from request_line

Для extract_regexp задаємо поле, яке він буде парсити – from request_line, аби не парсити весь зміст _msg.

Отримуємо такий результат:

filter pipe

Якщо хочемо відфільтрувати дані – використовуємо filter, в який можемо передати регулярку.

Наприклад, аби вибрати тільки деякі домени – робимо filter domain_name :~ "regex". Або без регулярки – з := "example.com", що буде швидше.

Але filter ставимо його перед extract_regexp – аби відсіяти зайве і не виконувати дорогі в плані ресурсів пошуки extract_regexp по зайвим строкам:

{stream="alb"}
| extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id>"
| filter domain_name :~ ".+challenge.example.co|lightdash.example.co"
| extract_regexp `.*:443(?P<uri_path>/[^/?]+).* HTTP` from request_line

Або можемо прибрати якісь результати, наприклад видалити всі домени, в яких є тире – робимо filter domain_name :!~ "-":

{stream="alb"}
| extract "<_> ... "
| filter domain_name :!~ "-"
| extract_regexp `.*:443(?P<uri_path>/[^/?]+).* HTTP` from request_line

stats pipe та stats pipe functions

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

Для цього у нас є stats pipe і її функції, див. всі в документації stats pipe functions.

Наприклад, avg():

{stream="alb"}
| extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id>"
| filter domain_name :!~ "^-$"
| extract_regexp `.*:443(?P<uri_path>/[^/?]+).* HTTP` from request_line
|stats by (domain_name) avg()

Результат:

Але зараз avg() рахує значення зі всіх numeric log fields:

avg returns the average value over the given numeric log fields .

Тому користі з цього нуль.

Натомість можемо вказати яке саме поле використовувати, вказуємо його аргументом до avg() –  avg(target_processing_time):

{stream="alb"}
...
|stats by (domain_name) avg(target_processing_time) as avg_target_processing_time

Результат:

math pipe

Ще хочеться бачити загальний час виконання запитів, але в полях ALB у нас три окремі значення – request_processing_time, target_processing_time та response_processing_time.

Робити три окремі метрики, а потім рахувати в Grafana не хочеться, тому можемо зробити простіше – використати LogsQL math(): спочатку рахуємо суму по всім трьом полям, а потім отриманий результат передаємо до avg():

{stream="alb"}
...
| math (request_processing_time + target_processing_time + response_processing_time) as total_processing_time
|stats by (domain_name) avg(total_processing_time) as avg_total_processing_time

Результат:

Хоча насправді сенсу особо  нема, бо в 99.9% випадків 99.9% часу все одно займає target_processing_time.

AWS Athena та AWS ALB logs

“Довіряй – але перевіряй”.

Тому зробимо табличку в AWS Athena, і потім з неї будемо робити запити для перевірки.

Документація – Query Application Load Balancer logs.

Виконуємо запит:

CREATE EXTERNAL TABLE IF NOT EXISTS alb_access_logs (
    type string,
    time string,
    elb string,
    client_ip string,
    client_port int,
    target_ip string,
    target_port int,
    request_processing_time double,
    target_processing_time double,
    response_processing_time double,
    elb_status_code int,
    target_status_code string,
    received_bytes bigint,
    sent_bytes bigint,
    request_verb string,
    request_url string,
    request_proto string,
    user_agent string,
    ssl_cipher string,
    ssl_protocol string,
    target_group_arn string,
    trace_id string,
    domain_name string,
    chosen_cert_arn string,
    matched_rule_priority string,
    request_creation_time string,
    actions_executed string,
    redirect_url string,
    lambda_error_reason string,
    target_port_list string,
    target_status_code_list string,
    classification string,
    classification_reason string,
    conn_trace_id string
)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe'
WITH SERDEPROPERTIES (
    'serialization.format' = '1',
    'input.regex' =
'([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) (.*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-_]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^\\s]+?)\" \"([^\\s]+)\" \"([^ ]*)\" \"([^ ]*)\" ?([^ ]*)? ?( .*)?'
)
LOCATION 's3://ops-1-33-devops-ingress-ops-alb-loki-logs/AWSLogs/492***148/elasticloadbalancing/us-east-1/';

Тепер маємо таблицю, яка дозволяє виконувати SQL-запити до логів, що зберігаються в S3:

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

SELECT elb_status_code, count(*)
FROM alb_access_logs
WHERE date(from_iso8601_timestamp(time)) = date('2025-12-04')
GROUP BY elb_status_code
ORDER BY elb_status_code;

Результат:

Ну і для додатково користуємось метриками з CloudWatch.

Recording Rules з VictoriaLogs

Розібрались з тим, як нам отримати поля, сформували поля – можна переходити до Recording Rules та метрик.

Документація – Recording rules.

AWS ALB logs 5-minute delay issue

Але перед тим, як почати писати rules – є момент, який треба уточнити:

  • у нас логи з S3 збираються раз на 5 хвилин (“ELB publishes a log file for each load balancer node every 5 minutes” – документація Access log files)
  • дефолтний Recording Rules evaluation time – 1 хвилина (див. Configuration)
  • і якщо момент виконання rule не співпаде з моментом запуску log parser і у VictoriaLogs логів на цей момент не буде – то і наш rule не спрацює, і нічого не створить

Але для таких випадків ми в налаштуваннях Rules group можемо використати eval_delay, наприклад:

apiVersion: operator.victoriametrics.com/v1beta1
kind: VMRule
metadata:
  name: vmlogs-alert-rules
spec:
  groups:
    - name: VM-ALB-logs-Test
      type: vlogs
      eval_delay: 15m
...

Тоді задача, яка мала запуститись у, наприклад, 12:00:00 – буде запущена в 12:15:00, але вона буде шукати логи в проміжок часу 12:00:00 – 12:01:00.

Ну а в логах, в нашому конкретному випадку, timestamp парситься з самих логів, тому в VictoriaLogs час буде такий, як було збережено самим лоад-балансером.

Планування Recording Rules

Повернемось до початку – що хочеться бачити:

  • помилки 4хх – ALB та target
  • помилки 5хх – ALB та target
  • загальна кількість реквестів на ALB та по конкретних ендпоінтах нашого Backend API
  • час відповіді загальна
  • час target response time на конкретних ендпоінтах

Тобто задача, фактично, зводиться к трьом показникам – кількість помилок, кількість реквестів, час відповіді.

Як ми це можемо зробити з Recording Rules і метриками:

  • 4хх і 5хх помилки по полям elb_status_code:
    • з фільтром filter elb_status_code :~ "40[1-4]|50[0-4]" отримуємо всі логи, в яких є elb_status_code з 4хх або 5хх
    • потім зі stats і rate() рахуємо кількість знайдених log records, в яких були 4хх або 5хх помилки
    • потім в дашбордах і алертах робимо фільтри по 4хх або 5хх (хоча можна і окремі метрики робити)
  • target_status_code – аналогічно
  • загальна кількість реквестів на секунду по домену:
    • кожен запис в логах == окремий HTTP query
    • тому можемо просто робити stats by (elb_id, domain_name) rate() – групуємо по elb_id, domain_name, аби потім мати фільтри в Grafana і алертах
  • загальна кількість реквестів на ендпоінт
    • аналогічно, тільки додаємо uri_pathstats by (elb_id, domain_name, uri_path) rate()
    • в uri_path у нас тільки перша частина запиту, їх не так багато різних, тому проблеми з high cardinality у VictoriaLogs не буде
  • ALB total response time і на конкретних ендпоінтах – тут є варіанти 🙂 див далі

Пам’ятаємо про high cardinality і в VictoriaMetrics: все, що ми використаємо у stats by () потім буде збережено в labels метрик, а тому робити групування по всяким всякі trace_id не варто – при потребі це можна буде перевіряти в самих логах, а в алертах і Grafana воно нам не треба.

Див. також VictoriaMetrics: Churn Rate, High cardinality, метрики та IndexDB.

Поїхали.

ALB Requests Rate rule

Почнемо з самої простої – просто рахуємо скільки записів було отримано за останній evalution interval, тобто 1 хвилину, потім з rate() отримуємо кількість змін на секунду:

apiVersion: operator.victoriametrics.com/v1beta1
kind: VMRule
metadata:
  name: vmlogs-alert-rules
spec:
  groups:
    - name: VM-ALB-logs
      type: vlogs
      eval_delay: 15m
      interval: 1m
        - record: vmlogs:alb:stats:elb_requests_total:rate
          debug: true
          expr: |
            {stream="alb"}
            | extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id> <_> <_> <_>"
            | stats by (elb_id, domain_name) rate() as elb_requests_rate

Деплоїмо, чекаємо 5-10 хвилин, перевіряємо в VictoriaMetrics:

О 10:13 маємо значення 4.83.

Перевіряємо в CloudWatch, де з math просто робимо “sum requests in 1 minute /60”:

Ті ж 4.83, правда о 10:12.

Можемо ще перевірити з Athena, яку підготували раніше, і з таким запитом:

SELECT
  date_trunc('minute', from_iso8601_timestamp(time)) AS minute,
  count(*) / 60.0 AS rps_estimated
FROM alb_access_logs
WHERE from_iso8601_timestamp(time) >= now() - interval '1' hour
GROUP BY 1
ORDER BY 1 DESC;

Ті ж цифри, тільки теж о 10:12:

ALB 4xx/5xx Errors rule

Далі робимо per second rate помилок – та ж сама логіка, тільки додаємо filter elb_status_code :~ "40[1-4]":

- record: vmlogs:alb:stats:elb_errors:4xx:rate
  debug: false
  expr: |
    {stream="alb"}
    | extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id> <_> <_> <_>"
    | filter elb_status_code :~ "40[1-4]"
    | extract_regexp `.*:443(?P<uri_path>/[^/?]+).* HTTP` from request_line
    | stats by (elb_id, elb_status_code, domain_name, uri_path) rate() as elb_4xx_errors_rate

Деплоїмо, чекаємо, перевіряємо у VictoriaMetrics:

Маємо значення 0.91 об 11:07.

В CloudWtach – те саме, тільки знов на 1 хвилину раніше:

ALB Target Processing Time rule

Наступним хочеться отримати те, що в CloudWatch у нас приходить в метриці TargetResponseTime, але мати можливість отримати і загальний час по всьому LoadBalancer – і по окремим URI.

Проблема зі stats by () і avg()

Перше рішення, яке прийшло в голову – це просто взяти середнє по кожному ендпоінту, потім взяти середнє по всім отриманням значенням – і має бути +/- те самий результат, що в CloudWatch Target Response Time з Average.

Але якщо зробити метрику так:

- record: vmlogs:alb:stats:elb_target_processing_time:avg
  debug: false
  expr: |
    {stream="alb"}
    ...
    | stats by (elb_id, domain_name, uri_path) avg(target_processing_time) as avg_target_processing_time

І потім для отримання загального середнього значення у VictoriaMetrics виконувати avg():

avg(vmlogs:alb:stats:elb_target_processing_time:avg)

То дані в VictoriaMetrics і CloudWatch будуть різні.

Наприклад, в 12:52 у VictoriaMetrics маємо значення 0.06:

Але в CloudWatch – Target Response Time і Average за 1 хвилину у 12:21 буде 0.11:

Або 0.15 в 12:52:

Зовсім не схоже на те, що бачимо у VictoriaMetrics.

Окей – тоді, може, просто взяти суму від всіх наших avg(by uri_path)?

sum(vmlogs:alb:stats:elb_target_processing_time:avg)

Але тоді о 12:52 результат буде взагалі 1.23:

Чому так виходить?

Бо у нас в stats() результати рахуються для кожної групи (elb_id, domain_name, uri_path).

Тобто, якщо взяти такі приклади і використати тільки uri_path:

  • /uri-1: 200 requests по 0.1 секунді:
    • sum: 200*0.1       == 20 sec
    • avg: 200*0.1/200 == 0.1 sec
  • /uri-2: 10 requests по 2 секунди:
    • sum: 10*2       == 20 sec
    • avg: 10*2/10  == 2 sec

Якщо ми будемо рахувати результат як avg(vmlogs:alb:stats:elb_target_processing_time:avg) – то для отримання avg() ми візьмемо значення з кожної метрики:

  • vmlogs:alb:stats:elb_target_processing_time:avg{uri_path="/uri-1"} == 0.1 sec
  • vmlogs:alb:stats:elb_target_processing_time:avg{uri_path="/uri-2"} == 2 sec

І поділимо на кількість отриманих метрик, тобто на 2, що дасть нам:

  • (0.1 sec + 2 sec) / 2 == 1.05 sec

Якщо ж ми будемо рахувати як sum(vmlogs:alb:stats:elb_target_processing_time:avg) – то для тої ж пари:

  • vmlogs:alb:stats:elb_target_processing_time:avg{uri_path="/uri-1"} == 0.1 sec
  • vmlogs:alb:stats:elb_target_processing_time:avg{uri_path="/uri-2"} == 2 sec

Ми отримаємо:

  • (0.1 sec + 2 sec) = 2.1 sec

Average values in CloudWatch

А як CloudWatch обчислює Average взагалі, і по TargetResponseTime?

Він бере кількість отриманих метрик, Sample count – о 12:52 це було 143:

Потім бере суму по всім отриманим метрикам, Sum – в 12:52 це було 22:

І ділить суму на кількість – 22/143 == 0.1538:

Або якщо повернутись до нашого прикладу вище – то це сума часу по обом ендпоінтам поділена на загальну кількість запитів: (/uri-1 sum + /uri-2 sum) / (/uri-1 requests + /uri-1 requests):

  • (20+20)/210 == 0.19

Тобто реальний середній час відповіді нашого умовного ALB – 0.19 секунди, а не 1.05 у випадку з avg() або тим більш не 2.1 sec у випадку з sum().

Отже, якщо ми хочемо мати і загальний середній час по всьому ALB, і середній час по кожному ендпоінту – то у нас два варіанти:

  • або в stats() треба брати суму, а не avg(), а потім отриманий результат ділити на загальну кількість запитів
  • або мати окрему метрику на загальний середній час – і окрему на середній час по кожному ендпоінту

При першому варіанті – це може виглядати так:

- record: vmlogs:alb:stats:elb_target_processing_time:sum
  debug: false
  expr: |
    {stream="alb"}
    | extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id> <_> <_> <_>"
    | extract_regexp `.*:443(?P<uri_path>/[^/?]+).* HTTP` from request_line
    | filter target_processing_time :! "-1"
    | stats by (elb_id, domain_name, uri_path) sum(target_processing_time) as sum_target_processing_time

- record: vmlogs:alb:stats:elb_target_processing_time:count
  debug: false
  expr: |
    {stream="alb"}
    | extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id> <_> <_> <_>"
    | extract_regexp `.*:443(?P<uri_path>/[^/?]+).* HTTP` from request_line
    | filter target_processing_time :! "-1"
    | stats by (elb_id, domain_name, uri_path) count() as count_requests

Замість vmlogs:alb:stats:elb_target_processing_time:count можна було б взяти vmlogs:alb:stats:elb_requests_total:count, яку ми робили вище – але в метриках target_processing_time ми прибираємо значення “-1”, яке буває в target_processing_time якщо підключення розірване чи завершилось помилкою, а в vmlogs:alb:stats:elb_requests_total:count у нас всі реквести без виключення.

І з цими двома метриками ми можемо отримати середнє значення часу відповіді по кожному ендпоінту так:

sum(
  sum(vmlogs:alb:stats:elb_target_processing_time:sum) by (uri_path)
  / 
  sum(vmlogs:alb:stats:elb_target_processing_time:count) by (uri_path)
)
by (uri_path)

Тут у нас в 12:47 ендпоінт /chats відповідав 0.54 секунди.

І те саме значення буде в самому першому варіанті:

- record: vmlogs:alb:stats:elb_target_processing_time:avg
  debug: false
  expr: |
    {stream="alb"}
    | extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id> <_> <_> <_>"
    | extract_regexp `.*:443(?P<uri_path>/[^/?]+).* HTTP` from request_line
    | filter target_processing_time :! "-1"
    | stats by (elb_id, domain_name, uri_path) avg(target_processing_time) as avg_target_processing_time

Маємо о 12:47 ті самі 0.54 на /chats:

А аби отримати середній загальний час відповіді по всьому лоад-балансеру – можемо або використати метрики vmlogs:alb:stats:elb_target_processing_time:sum і vmlogs:alb:stats:elb_target_processing_time:count так:

sum(
  sum(vmlogs:alb:stats:elb_target_processing_time:sum)
  / 
  sum(vmlogs:alb:stats:elb_target_processing_time:count)
)

Отримаємо 0.22 у 12:58:

Або просто зробити окрему метрику зі stats by (elb_id) avg():

- record: vmlogs:alb:stats:elb_target_processing_time_total:avg
  debug: false
  expr: |
    {stream="alb"}
    | extract "<_> <_> <elb_id> <_> <_> <request_processing_time> <target_processing_time> <response_processing_time> <elb_status_code> <target_status_code> <received_bytes> <sent_bytes> <request_line> <user_agent> <_> <_> <_> <trace_id> <domain_name> <_> <_> <_> <_> <_> <error_reason> <_> <_> <_> <_> <conn_trace_id> <_> <_> <_>"
    | extract_regexp `.*:443(?P<uri_path>/[^/?]+).* HTTP` from request_line
    | filter target_processing_time :! "-1"
    | stats by (elb_id) avg(target_processing_time) as avg_target_processing_time

Яка поверне ті самі 0.22 в 12:58:

І те саме значення буде в CloudWatch:

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

Тепер можна додавати інші метрики, алерти, і тюнити Grafana dashboard.

Loading

FreeBSD: установка на ThinkPad X200 Tablet у 2025 році
0 (0)

30 Листопада 2025

Я давно фанат ThinkPad, дуже люблю всю їхню лінійку.

Нещодавно десь зустрів модель X200, які випускались з 2008 року – просто десь побачив картинку, і дуже захотів собі в “колекцію”. Неочікувано – але він навіть знайшовся в продажу на OLX, тому купив собі цей чудо-девайс.

ThinkPad X200 overview

Він… Ну – це просто бімба 🙂

Перше, що відрізняє цей ноутбук від інших моделей – це поворотний екран на 360 градусів, і це “фірмова фішка” моделі саме X200 Tablet, бо є X200 без цього.

Друге – це тачскрін.

Зовнішній вигляд

Виглядає ThinkPad X200 Tablet так:

На борту навіть є dial-up модем!

Ну і, звісно, ніяких HDMI – для зовнішнього монітору тільки VGA.

Хоча HDMI 1.0 з’явився ще у 2003, але тоді він використовувався для DVD-плеєрів та телевізорах, а ноутбуки в ті роки ще йшли переважно з VGA.

Замовив перехідник VGA на HDMI, бо в моєму моніторі DELL U3421WE ніяких VGA, само собою, вже нема – подивимось, чи спрацює.

Справа є фізичний перемикач для відключення WiFi/Bluetooth, хоча в моїй моделі блютузу нема:

В комплекті йде стилус:

ThinkPad X200 Hardware

Звісно, залізо вже стареньке:

  • дисплей: 12.1 дюймів, 1440×900 точок
  • процесор: Intel Core 2 Duo SU9400, 1.4 GHz
  • відео: Integrated card Intel 4500MHD
  • оперативна пам’ять: DDR3-1066 MHz
    • в офіційній документації Lenovo завжди писали, що максимум пам’яті 4 гігабайти, але гугол каже, що 8 гіг працює без проблем
    • поки що в мене 4 GB, замовив нові 2 планки по 4 ГБ, подивимось чи запрацює
    • зараз (2025 рік) DDR3-1066 MHz на 4 ГБ коштує 440 гривень 🙂
  • диск: SATA, але сам диск HDD, 250 ГБ, 5400 RPM
    • начебто є можливість замінити диск на SSD, але корпус ще не розбирав, потім подивлюся, і якщо можна – то поставлю якийсь SSD
  • мережа:
    • Ethernet 1000 bmps
    • WiFi: 802.11a, 802.11g, 802.11n

Встановлення FreeBSD

Чому FreeBSD? Бо це моя перша UNIX-система, на якій я вперше збирав ядро, ще у 2006 чи 2007 році, і потім до 2012 чи 2013 це була моя основна система на моїх перших сервера.

Моя перша версія FreeBSD була… не пам’ятаю точно, чи то 5, чи то 6. Але згодом, після виходу FreeBSD 9 перейшов на Linux (CentOS). Правда чому саме – теж не пам’ятаю. Щось пов’язане зі змінами в роботі з пакетами і FreeBSD ports.

І навіть в цьому блозі одним із перших матеріалів були пости саме по FreeBSD – FreeBSD: установка коллекции портов (Ports Collection), 14 серпня 2011.

Ну і раз вже налаштовуємо “раритетний” ноутбук – то чому б не спробувати і “раритетну” систему? 🙂

Трохи ностальгії по тим часам, коли вчився працювати з портами FreeBSD, коли вперше розбирався з runlevels в Linux часів SysV init і їхніми аналогами у FreeBSD – режимами завантаження системи.

Хоча, звісно, встановлювати будемо актуальну версію FreeBSD, 14.3 на сьогодняшній день.

Єдине, що тачскрін завести не вдалося, і, судячи з результатів гуглінгу, не вдасться – бо FreeBSD просто не має драйвера.

Тому я все ж встановлю FreeBSD, пограюсь і опишу якісь деталі, але потім на цьому ноутбуці буде Arch Linux – там хоч і довелось трохи попрацювати напильником, але тачскрін працює відмінно.

Вибір образу FreeBSD

Для FreeBSD існує три гілки системи:

  • RELEASE: основна стабільна версія, апдейти тільки security + critical fixes – максимум стабільності, мінімум сюрпризів
  • STABLE: гілка розробки, з якої формується майбутній RELEASE – вже достатньо стабільна, але все ще можуть бути баги
  • CURRENT: гілка активної розробки з усіма новими фічами і плюшками – мінімум стабільності, максимум нових експерементальних штук

FreeBSD має окремі версії для майже всіх платформ:

Для нашого ThinkPad X200 вибираємо amd64, завантажуємо образ зі сторінки Get FreeBSD, і починаємо установку.

Встановлення FreeBSD на віртуальну машину

Аби не робити 100500 фотографій екрану – процес установки FreeBSD покажу на віртуалці з QEMU/KVM, бо принципової різниці нема.

По віртуалізації QEMU/KVM в Linux є чорнетка, допишу якось, давно лежить.

Офіційна документація – Chapter 2. Installing FreeBSD.

У FreeBSD дуже приємний інсталятор bsdinstall, з яким можна зробити все і відразу. І, як на мене – він зручніший за archintsall, з яким я взагалі не подружився і Arch Linux завжди встановлюю руками:

Вибираємо 1, далі маємо вибір – або запустити установку, або використати ISO як live-cd:

Задаємо ім’я хоста:

Далі вибір компонентів.

Нам тут потрібні тільки порти, “ports – Ports tree“, і можна додати “src – System source tree” – тут весь код FreeBSD, корисно, якщо хочеться зібрати власне ядро і для установку апдейдів з утилітою freebsd-update, про неї трохи далі:

Наступний крок – розбивка диску:

  • ZFS: файлова система з підтримкою snapshots, RAID і можливістю зміни розміру розділів
    • з’явилась у 2005 році для операційної системи Solaris від Sun Microsystems, і на той час була революційною системою
  • UFS: класична для FreeBSD файлова система – дуже стабільна, мінімум використання RAM, але і не має всіх фіч ZFS

Залишаємо дефолтну опцію “Auto (ZFS)”:

Забігаючи наперед – ось так потім виглядають розділи вже на самому ноутбуку:

# gpart show
=>       40  312581728  ada0  GPT  (149G)
         40       1024     1  freebsd-boot  (512K)
       1064        984        - free -  (492K)
       2048    4194304     2  freebsd-swap  (2.0G)
    4196352  308383744     3  freebsd-zfs  (147G)
  312580096       1672        - free -  (836K)

І навіть пропонується відразу налаштувати RAID – але це явно не наш кейс:

Вибираємо диск для установки:

Чекаємо завершення установки:

Задаємо пароль root:

Вибираємо мережевий інтерфейс:

Це установка на віртуалку, WiFi тут нема.

Але на самому ноуті можна відразу налаштувати і його, хоча в моєму випадку з ThinkPad X200 потім довелось трохи робити руками, далі покажу.

Залишаємо DHCP:

Вибираємо таймзону:

Після чого налаштовуємо базові параметри системи:

  • local_unbound: локальний DNS-кеш + DNSSEC валідація, для домашнього ноута можна не включати
  • sshd: включаємо
  • moused: підтримка миші в консолі – користі мало, але виглядає прикольно 🙂
  • ntpd: синхронізація часу
  • powerd: динамічне керування частотою CPU, дуже корисно, особливо для ноутбука – зберігати час роботи батареї
  • dumpdev: допомагає дебажити kernel panic

Далі налаштування для security – цікаво, але для домашнього ноута можна пропустити, має сенс для серверів:

Детально можна почитати в документації Section 2.8.4, “Enabling Hardening Security Options”, але коротко пройдемось по опціях, бо тут проявляються перші (якщо не говорити про файлу систему) відмінності від Linux:

  • hide_uids і hide_gids: ховає процеси інших користувачів системи (root, звісно, бачить все)
  • hide_jail: ховає процеси, які запущені в jail
    • FreeBSD Jails – аналог “класичних” контейнерів в Linux, але з більше жорсткою ізоляцією
    • і з’явились вони ще до того, як в Linux ядрі взагалі з’явилась підтримка чогось схожого:
      • jails були додані у FreeBSD ще у 2000 році, з FreeBSD 4.0
      • в Linux на той час був тільки chroot, де ізоляція була дуже слабка, і можна було легко вийти за межі “контейнера” і тримати доступ до всієї системи
      • і тільки у 2006-2008 в Linux з’явились cgroups, аж в 2008-2013 були додані namespaces, і тільки після цього, у 2013 році, з’явився Docker і контейнери в Linux, якими ми їх знаємо зараз
  • read_msgbuf: заборона читання dmesg звичайним юзерам
  • proc_debug: вимикає для звичайних користувачів можливість дебажити чужі процеси і обмежує частину інформації з /proc
  • random_pid: якщо включено, то PID задається з рандомним зміщенням номеру, а не по черзі
  • clear_tmp: у FreeBSD каталог /tmp по дефолту не очищається при ребуті, з clear_tmp це можна включити
    • в Linux залежить від дистрибутива і того, як саме монтується /tmp, бо зазвичай це tmpfs в RAM, який, звісно, очищається
    • FreeBSD свідомо не очищає його, бо політика FreeBSD – “адміністратор сам вирішує, що треба робити системі
  • disable_syslogd: заборона відкриття мережевого сокету для демона syslogd – на ноуті можна включити
    • але сам syslogd продовжує використання локального Unix socket /var/run/log для роботи
  • secure_console: блокує root-вхід з консолі без пароля при завантаженні системи в single-user mode (див. Chapter 15. The FreeBSD Booting Process – у FreeBSD дуже класна документація)
  • disable_dtrace: DTrace у FreeBSD – аналог strace/eBPF в Linux, для трасування процесів, системних викликів і роботи ядра, і має можливість вносити “на льоту” зміни в пам’ять та ядро; disable_dtrace блокує цю можливість навіть для root

Окей – залишаємо тут все по дефолту, і переходимо до user management:

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

У FreeBSD є можливість вказати Login class, і це фішка чисто FreeBSD, див. Configuring Login Classes.

Ну і власне на цьому установка завершена.

В останньому вікні є можливість щось дотюнити:

Ребутаємось, і маємо нову систему:

І екран загрузки вже на самому ноуті:

FreeBSD load options

Коротко по доступних опціях:

  • Boot Multi user (Enter): стандартний запуск, монтуються файлові системи, запускаються сервіси із /etc/rc.conf
    • аналог в Linux – звичайний boot у runlevel/systemd multi-user.target
  • Boot Single user: коли система зламалась, і треба щось пофіксити
    • запускається тільки ядро і мінімальний shell
    • /” монтується в read-only (можна перемонтувати в r/w)
    • аналог в Linux – init=/bin/bash або systemd.unit=rescue.target
  • Escape to loader prompt: консоль у FreeBSD loader, де можна змінити параметри ядра, вибрати інше ядро, змінити ZFS Boot Environments
    • аналог в Linux – GRUB console, але FreeBSD loader глибше інтегрований з системою
  • Reboot: reboot 🙂
  • Cons: Video: фішка FreeBSD, задає тип для system console – video console (звичайний екран) або serial console
  • Kernel: default/kernel: вибір ядра для завантаження
    • при апгрейдах попередня версія ядра зберігається у /boot/kernel.old/, і при проблемах можна завантажити інші ядра
  • Boot Options: додаткові параметри для завантаження – відключити ACPI, DMA, ZFS cache, etc

Повертаємось до ноутбука.

Налаштування WiFi

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

І мережі:

Але після налаштування і завершення установки інтерфейсу нема:

Робимо вручну.

З pciconf -lv перевіряємо пристрої на PCI/PCI-Express шині:

Нас цікавить device iwn – “Intel WiFi Link 5300“:

The Intel® WiFi Link 5300 Series is a family of IEEE 802.11a/b/g/Draft-N1 wireless network adapters that operate in both the 2.4 GHz and 5.0 GHz spectra

Intel® WiFi Link 5300 Series.

Значить, нам потрібен драйвер iwn і модуль iwn5000fw.

Додаємо їх завантаження у файл /boot/loader.conf:

if_iwn_load="YES"
iwn5000fw_load="YES"

Можна додати без ребуту з kldload:

# kldload if_iwn
# kldload iwn5000fw

Створюємо новий віртуальний Wi-Fi інтерфейс з іменем wlan0 на основі апаратного пристрою iwn0, який ми побачили в pciconf:

# ifconfig wlan0 create wlandev iwn0

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

Звісно, мережі ще нема – додаємо конфіг для неї.

Для роботи з WiFi у FreeBSD є утиліта wpa_supplicant, яка відповідає за авторизацію, підтримку підключення, перепідключення при втраті сигналу.

Редагуємо файл /etc/wpa_supplicant.conf, в кінець додаємо параметри:

network={
    ssid="MyWiFi"
    psk="qwerty12345"
}

Додаємо загрузку конфігу WiFi у /etc/rc.conf, який відповідає за autostart:

wlans_iwn0="wlan0"
ifconfig_wlan0="WPA DHCP"

Рестартимо мережу:

# service netif restart

І wlan0 має підключитись і отримати IP:

root@setevoy-x200:/home/setevoy # ifconfig wlan0
wlan0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        options=0
        ether 00:21:6a:b5:32:4c
        inet 192.168.0.172 netmask 0xffffff00 broadcast 192.168.0.255
        groups: wlan
        ssid setevoy-tp-link-21-5 channel 36 (5180 MHz 11a ht/40+) bssid 30:68:93:80:ef:73
        regdomain FCC country US authmode WPA2/802.11i privacy ON
        deftxkey UNDEF AES-CCM 2:128-bit txpower 17 bmiss 10 mcastrate 6
        mgmtrate 6 scanvalid 60 ampdulimit 64k -amsdutx amsdurx shortgi -stbc
        -ldpc -uapsd wme roaming MANUAL
        parent interface: iwn0
        media: IEEE 802.11 Wireless Ethernet MCS mode 11na
        status: associated
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>

Може бути із затримкою в 10-30 секунд, але в результаті завелось.

При потребі можна запустити wpa_supplicant в дебаг-режимі:

# wpa_supplicant -i wlan0 -c /etc/wpa_supplicant.conf -dd

FreeBSD Ports Collection та pkg

У FreeBSD є два незалежні механізми встановлення програм – з утилітою pkg, або збирати самому.

pkg – аналог apt або pacman в Linux: отримує і встановлює вже зібрані і готові до встановлення пакети з офіційного репозиторію:

# cat /etc/pkg/FreeBSD.conf
FreeBSD: {
  url: "pkg+https://pkg.FreeBSD.org/${ABI}/quarterly",
  mirror_type: "srv",
  signature_type: "fingerprints",
  fingerprints: "/usr/share/keys/pkg",
  enabled: yes
}
FreeBSD-kmods: {
  url: "pkg+https://pkg.FreeBSD.org/${ABI}/kmods_quarterly_${VERSION_MINOR}",
  mirror_type: "srv",
  signature_type: "fingerprints",
  fingerprints: "/usr/share/keys/pkg",
  enabled: yes
}

Тут FreeBSD – це user-facing пакети, а FreeBSD-kmods – репозиторій для додаткових модулів ядра (nvidia-driver, virtualbox-kmod тощо).

FreeBSD Ports Collection – каталог із source code пакетів та Makefiles, розбиті по категоріям, наприклад порт для xfce4-conf буде в директорії /usr/ports/x11/:

root@setevoy-x200:/home/setevoy # ls -1 /usr/ports/x11/xfce4-conf/
Makefile
distinfo
files
pkg-descr
pkg-plist

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

Апгрейд системи і пакетів

Тут у нас є дві окремі частини – сама система, і пакети, які ми встановлюємо з pkg.

Документація – Chapter 26. Updating and Upgrading FreeBSD і freebsd-update man.

Для апгрейду системи у FreeBSD є класна утиліта freebsd-update – дозволяє і отримувати останні апдейти, і виконувати апгрейд самої системи.

Аби отримати і встановити вручну – робимо:

# freebsd-update fetch
# freebsd-update install

fetch – завантажити останні апдейти, install – встановити їх.

Або можна відразу додати в cron:

@daily    root    freebsd-update cron

freebsd-update fetch та freebsd-update install виконають апгрейд ядра, системних утиліт в /bin, /sbin, /usr/bin, бібліотеки в /lib, /usr/lib і драйверів.

Для апгрейду пакетів, які ми встановлюємо з pkg – використовуємо аналогічні команди:

# pkg update
# pkg upgrade

Аналогічно до freebsd-update – з update отримуємо оновлення, з upgrade встановлюємо їх.

Установка X.Org та Desktop Environment

Про X.Org і вибір Desktop Environment писав у пості Arch Linux: установка і налаштування KDE Plasma у 2025, зараз нам треба просто вибрати яке саме оточення собі взяти.

Враховуючи обмежені ресурси – старенький CPU та всього 4 GB RAM – треба щось легке.

Можна XFCE – легкий, стабільний, з коробки є всі необхідні інструменти.

LXQT – трохи важчий за XFCE, але більш сучасний.

Ну або просто взяти чистий Windows Manager типу Fluxbox або Openbox.

Давайте візьмемо XFCE.

Встановлюємо X.Org та драйвери для відео – Direct Rendering Manager GPU drivers.

X.org потягне велику пачку залежностей

# pkg install xorg drm-kmod
...
Number of packages to be installed: 318

The process will require 3 GiB more space.
431 MiB to be downloaded.
...

Встановлюємо сам XFCE – теж немаленький:

# pkg install xfce xfce4-goodies
...
Number of packages to be installed: 317

The process will require 1 GiB more space.
248 MiB to be downloaded.
...

Але році у 2010 встановлював KDE з ports – то процес зборки зайняв багато годин, бо pkg тоді ще не було, і в ті часи майже все встановлювалось із ports collection.

Була і тоді система pkg_tools, але в ній не було системи залежностей, пакети качались по FTP, а апгрейд пакетів виконувався фактично через видалення і встановлення заново.

Повертаємось на нашої системи.

Встановили XFCE – додаємо login manager, slim (Simple Log In Manager):

# pkg install slim slim-themes

До /etc/rc.conf додаємо запуск DBus (потрібен для XFCE) та Slim:

sysrc dbus_enable="YES"
sysrc slim_enable="YES"

Створюємо файл ~/.xinitrc в домашній директорії юзера, аби Slim знав що запускати:

# echo "exec startxfce4" > /home/setevoy/.xinitrc

Ребутаємо машинку:

# reboot

І вуаля – все готово:

Можна перевірити з яким драйвером працює відео:

# cat /var/log/Xorg.0.log | grep -E "Driver|scfb|vesa"
[   145.813]    X.Org Video Driver: 25.2
[   145.862] (==) Matched scfb as autoconfigured driver 2
[   145.862] (==) Matched vesa as autoconfigured driver 3
[   145.864]    Module class: X.Org Video Driver
[   145.864]    ABI class: X.Org Video Driver, version 25.2
[   145.864] (II) LoadModule: "scfb"
[   145.864] (II) Loading /usr/local/lib/xorg/modules/drivers/scfb_drv.so
[   145.864] (II) Module scfb: vendor="X.Org Foundation"
[   145.864]    ABI class: X.Org Video Driver, version 25.2
[   145.864] (II) LoadModule: "vesa"
[   145.864] (II) Loading /usr/local/lib/xorg/modules/drivers/vesa_drv.so
[   145.864] (II) Module vesa: vendor="X.Org Foundation"
[   145.864]    Module class: X.Org Video Driver
[   145.865]    ABI class: X.Org Video Driver, version 25.2
[   145.865] (II) modesetting: Driver for Modesetting Kernel Drivers: kms
[   145.865] (II) scfb: driver for wsdisplay framebuffer: scfb
[   145.865] (II) VESA: driver for VESA chipsets: vesa
[   145.873] (WW) Falling back to old probe method for scfb
[   145.873] scfb trace: probe start
[   145.873] scfb trace: probe done
[   145.874]    ABI class: X.Org Video Driver, version 25.2
[   145.875]    ABI class: X.Org Video Driver, version 25.2
[   146.006] (II) UnloadModule: "scfb"
[   146.006] (II) Unloading scfb

scfb чимось не сподобався, буде з vesa, але на цьому залізі взагалі не принципово.

Додаємо інші корисні пакети:

# pkg install vim sudo bash

Я вже звик до bash, тому встановлюю і його.

Аби змінити юзеру його shell – використовуємо chsh:

# chsh -s /usr/local/bin/bash setevoy
chsh: user information updated

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

# grep setevoy /etc/passwd
setevoy:*:1001:1001:setevoy:/home/setevoy:/usr/local/bin/bash

Ну і власне на цьому все.

Система працює, можна користуватись.

Але, як казав на початку – на такому старому залізі під FreeBSD не працює тачскрін, тому потім все ж встановлю Arch Linux – з ним вже тестив, там тачскрін пальцями і стилусом працює без проблем.

Loading