Glance: налаштування self-hosted home page для браузера
0 (0)

11 Лютого 2026

Є така прикольна штука, як self-hosted home pages.

Колись побачив їх десь на Reddit, зберіг в закладки, і ось тепер, як в мене є всяка self-hosted тема з NAS (див. FreeBSD: Home NAS, part 1), Grafana і іншими корисними в роботі і побуті речами – то подумав, що було б непогано зробити і собі таку дашборду.

З тих, що в мене збережені, і вони, здається, найбільш популярні – це gethomepage/homepage та glanceapp/glance.

Але Homepage якась більш важка, з купою компонентів – фронтенд, бекенд, якісь рендери, а Glance – простіша, і при цьому, в принципі, має все, що я хотів побачити – хоча місцями це робиться через костилі 🙂

Власне – налаштування Glance.

Робити поки буду локально на ноутбуці з Docker Compose, пізніше перенесу конфіг або на FreeBSD/NAS чи на Raspberry PI з Debian.

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

Див. документацію Pages & Columns.

Основні компоненти – це Pages, Columns та Widgets:

  • pages: власне, сторінки – можна мати кілька вкладок
  • columns: кожна page розбивається на кілька колонок
  • widgets: і в кожній колонці є набір віджетів

Віджети теж можна групувати і робити вкладки – далі побачимо на прикладі Reddit.

Для погратись – легко запускається з Docker, див. Installation.

Окремих тем нема – все налаштовується через стилі, наприклад:

...
theme:
  background-color: 225 14 15
  primary-color: 157 47 65
  contrast-multiplier: 1.1
...

Але є готові стилі – див. Themes.

Окрім дефолтних віджетів є community widgets, наприклад я там собі взяв NextDNS Stats.

Мої Pages та Widgets

Коротко – приклад того, як це все можна налаштувати.

Так як в мене це все хоститься локально і кудись в GitHub я конфіг зберігати не буду – то всякі токени записав прямо в конфіг, але взагалі для них можна використати змінні оточення – див. Other ways of providing tokens/passwords/secrets.

Перша сторінка – Home з трьома колонками:

Clock, Weather, Calendar

Час, погода і календар:

...
pages:
  - name: Home
    columns:
      # ---------- LEFT ----------
      - size: small
        widgets:
          - type: clock
            hour-format: 24h
            timezones:
              - timezone: Asia/Bangkok
                label: Chiang Mai
              - timezone: America/New_York
                label: New York

          - type: weather
            location: Kyiv
            units: metric

          - type: calendar
...

В clock віджеті додав ще дві зони – бо в США у нас частина розробників, а в Chiang Mai – розробник один, але часто з ним спілкуюсь і постійно згадую яка в нього зараз година.

Центрально колонка – з типом full, і для кожної page треба мати як мінімум одну колонку з full.

Search

Тут віджет search:

...
      # ---------- CENTER ----------
      - size: full
        widgets:
          - type: search
            search-engine: duckduckgo
            new-tab: true
            autofocus: true
            bangs:
              - title: YouTube
                shortcut: "!yt"
                url: https://www.youtube.com/results?search_query={QUERY}
...

Bookmarks

Bookmarks – основні для швидкого доступу, розбиті по категоріям:

...
          - type: bookmarks
            groups:
              - title: Local
                color: 120 70 50
                target: _self
                links:
                  - title: MikroTik Gateway
                    url: http://192.168.0.1/webfig/#IP:DHCP_Server.Leases
...
              - title: AI
                color: 260 50 70
                target: _self
                links:
                  - title: ChatGPT
                    url: https://chat.openai.com/chat
...
              - title: RTFM
                color: 200 50 50
                target: _self
                links:
                  - title: RTFM Main
                    url: https://rtfm.co.ua/
...

Для вибору color можна скористатись colorpicker.dev: перша цифра – сам колір, друга – saturation (насиченість), третя – lightness (яскравість).

Group для Reddit

Далі приклад групування з Group – зробив собі окремі вкладки для різних сабредітів, але двома окремими групами – умовний “Reddit Ukraine” і “Reddit IT”:

...
          - type: group
            widgets:
              - type: reddit
                subreddit: finance_ukr
                show-thumbnails: true
              - type: reddit
                subreddit: durka_ukr
                show-thumbnails: true

          - type: group
            widgets:
              - type: reddit
                subreddit: aws
                show-thumbnails: false
              - type: reddit
                subreddit: archlinux
                show-thumbnails: false
...

Split Column для новин

Наступний віджет – Split Column, де більше стисло новини і якісь цікаві матеріали:

...
          - type: split-column
            max-columns: 2
            widgets:
              - type: lobsters
                sort-by: hot
                limit: 15
                collapse-after: 5
              - type: rss
                title: News
                style: vertical-list
                feeds:
                  - url: https://aws.amazon.com/blogs/aws/feed/
                    title: AWS Blogs
                  - url: https://skeletor.org.ua/?feed=rss2
                    title: Skeletor
...

Ну і більш цікава частина – справа, інформація по статусу сервісів.

GitHub Releases

Віджет releases – останні релізи в GitHub:

...
      - size: small
        widgets:
          - type: releases
            token: github_pat_11A***1jF
            repositories:
              - VictoriaMetrics/VictoriaMetrics
              - pdf/zfs_exporter
              - tess1o/go-ecoflow-exporter
...

Мені за zfs_exporter і go-ecoflow-exporter є сенс слідкувати, бо вони в мене деплояться вручну, див. FreeBSD: Home NAS, part 11 – extended моніторинг з додатковими експортерами.

Хоча, звісно, ніхто не відміняє можливість просто підписатись на апдейти в самому GitHub 🙂

Custom API для NextDNS

Приклад кастомного віджета custom-api – інформація по моєму NextDNS:

...
          - type: custom-api
            title: NextDNS Analytics
            title-url: https://api.nextdns.io/profiles/***/analytics/status
            cache: 1h
            url: https://api.nextdns.io/profiles/***/analytics/status
            headers:
              X-Api-Key: 3f8***457
            template: |
              {{ if eq .Response.StatusCode 200 }}
                <div style="display: flex; justify-content: space-between;">
                  {{ $total := 0.0 }}
                  {{ $blocked := 0.0 }}
                  {{ range .JSON.Array "data" }}
                    {{ $total = add $total (.Int "queries" | toFloat) }}
                    {{ if eq (.String "status") "blocked" }}
                      {{ $blocked = add $blocked (.Int "queries" | toFloat) }}
              ...
                <div style="text-align: center; color: var(--color-negative);">
                  Error: {{ .Response.StatusCode }} - {{ .Response.Status }}
                </div>
              {{ end }}
...

І далі ще буде приклад з власним міні-API сервісом.

Monitor – статуси HTTP-сервісів

Віджет monitor – прикольна штука для відображення статусу сервісів, робить простий GET-запит на вказаний URL, ну і працює (принаймні поки що) тільки з HTTP/S:

...
          - type: monitor
            cache: 1m
            title: Services
            sites:
              - title: RTFM
                url: https://rtfm.co.ua
                icon: /assets/rtfm-icon-2.png
              - title: MikroTik RB4011
                url: http://192.168.0.1
                icon: sh:mikrotik-light
              - title: Grafana
                url: http://nas.setevoy:3000
                icon: sh:grafana
              - title: Jellyfin
                url: http://nas.setevoy:8096
                icon: sh:jellyfin
...

На MikroTik RB4011 (див. MikroTik: перше знайомство та Getting Started), наприклад, з локальної мережі доступний web-інтерфейс, тому через нього можна перевіряти статус.

Аби підключити іконки – шукаємо дефолтні на, наприклад, selfh.st або dashboardicons.com.

Або можна задати кастомні іконки – додаємо файли в каталог /assets і включаємо його в конфігу Glance:

...
server:
  host: 0.0.0.0
  port: 8080
  assets-path: /app/assets
...

Server Stats – CPU, RAM, Disk

Цікава штука server-stats, хоча вона ще в beta:

...
          - type: server-stats
            servers:
              - type: local
                name: setevoy-work
...

Потребує додаткового сервісу – glanceapp/agent, я його поки що просто додав до docker-compose.yaml:

services:
  glance:
    container_name: glance
    image: glanceapp/glance
    restart: unless-stopped
    volumes:
      - ./config:/app/config
      - ./assets:/app/assets
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    ports:
      - 8080:8080
    env_file: .env

  glance-agent:
    container_name: glance-agent
    image: glanceapp/agent
    ports:
      - 27973:27973

Docker containers

І останній тут – запущені Docker-контейнери, бо іноді забуваю, що щось запущено:

...
          - type: docker-containers
            hide-by-default: false
            running-only: true
...

(про Uptime Kuma теж напишу)

Вся сторінка Home вийшла поки що такою:

Custom API для FreeBSD/NAS

Ну і з цікавих рішень: хочеться з FreeBSD виводити якусь цікаву інформацію.

Active SSH connections

Спершу приклад “активні SSH-юзери” – перевіряємо хто підключений і звідки, виводимо тільки унікальні адреси:

root@setevoy-nas:~ # who | awk '$6 ~ /^\(/ {print $1, $6}' | sort -u
setevoy (10.100.0.3)
setevoy (192.168.0.3)

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

Тепер робимо простий python-скрипт, який буде нашим API-ендпоінтом.

Пишемо файл /usr/local/bin/glance_api.py:

#!/usr/bin/env python3.11
# minimal API server for Glance NAS page
# exposes active SSH sessions as JSON

from http.server import BaseHTTPRequestHandler, HTTPServer
import subprocess
import json

HOST = "192.168.0.2"
PORT = 9001


class Handler(BaseHTTPRequestHandler):

    def do_GET(self):

        if self.path == "/ssh":

            # run who and extract unique SSH sessions
            output = subprocess.check_output(
                "who | awk '$6 ~ /^\\(/ {print $1, $6}' | sort -u",
                shell=True
            ).decode().strip().splitlines()

            sessions = []

            for line in output:
                if line:
                    parts = line.split()
                    sessions.append({
                        "user": parts[0],
                        "ip": parts[1].strip("()")
                    })

            response = {
                "count": len(sessions),
                "sessions": sessions
            }

            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps(response).encode())

        else:
            self.send_response(404)
            self.end_headers()


if __name__ == "__main__":
    server = HTTPServer((HOST, PORT), Handler)
    server.serve_forever()

Запускаємо його:

root@setevoy-nas:~ # chmod +x /usr/local/bin/glance_api.py

root@setevoy-nas:~ # /usr/local/bin/glance_api.py

Перевіряємо локально:

root@setevoy-nas:~ # curl 192.168.0.2:9001/ssh
{"count": 2, "sessions": [{"user": "setevoy", "ip": "10.100.0.3"}, {"user": "setevoy", "ip": "192.168.0.3"}]}

Тепер додаємо в Glance новий віджет з типом custom-api:

...
      # ---------- RIGHT ----------
      - size: small
        widgets:
          - type: custom-api
            title: Active SSH
            url: http://nas.setevoy:9001/ssh
            template: |
              {{ $count := .JSON.Int "count" }}

              {{ if eq $count 0 }}
                No active SSH sessions
              {{ else }}
                <ul class="list list-gap-10">
                {{ range .JSON.Array "sessions" }}
                  <li>
                    <span class="color-highlight">{{ .String "user" }}</span>
                    <span class="color-muted">({{ .String "ip" }})</span>
                  </li>
                {{ end }}
                </ul>
              {{ end }}
...

І результат:

Uptime, CPU, ZFS pool status

Додатково можна вивести ще інформацію – uptime, CPU load, etc.

Додаємо в скрипт ще один ендпоінт /status, тепер весь скрипт такий:

#!/usr/bin/env python3.11
# minimal API server for Glance NAS page
# exposes active SSH sessions as JSON

from http.server import BaseHTTPRequestHandler, HTTPServer
import subprocess
import json

HOST = "192.168.0.2"
PORT = 9001


class Handler(BaseHTTPRequestHandler):

    def do_GET(self):

        if self.path == "/ssh":

            # run who and extract unique SSH sessions
            output = subprocess.check_output(
                "who | awk '$6 ~ /^\\(/ {print $1, $6}' | sort -u",
                shell=True
            ).decode().strip().splitlines()

            sessions = []

            for line in output:
                if line:
                    parts = line.split()
                    sessions.append({
                        "user": parts[0],
                        "ip": parts[1].strip("()")
                    })

            response = {
                "count": len(sessions),
                "sessions": sessions
            }

            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps(response).encode())

        elif self.path == "/status":

            # get uptime and load averages
            uptime_raw = subprocess.check_output(
                "uptime",
                shell=True
            ).decode().strip()

            # extract load averages
            load_part = uptime_raw.split("load averages:")[1].strip()
            load_values = [x.strip() for x in load_part.split(",")]

            # extract uptime text
            uptime_text = uptime_raw.split(" up ", 1)[1].split(", load averages:", 1)[0].rsplit(",", 1)[0].strip()

            # get zpool info
            zpool_raw = subprocess.check_output(
                "zpool list -H -o name,health,size,alloc,free,capacity",
                shell=True
            ).decode().strip().split()

            zpool = {
                "name": zpool_raw[0],
                "health": zpool_raw[1],
                "size": zpool_raw[2],
                "alloc": zpool_raw[3],
                "free": zpool_raw[4],
                "capacity": zpool_raw[5],
            }

            # get child datasets only
            datasets_raw = subprocess.check_output(
                "zfs list -H -o name,used,avail -r nas | tail -n +2",
                shell=True
            ).decode().strip().splitlines()

            datasets = []

            for line in datasets_raw:
                parts = line.split()
                datasets.append({
                    "name": parts[0],
                    "used": parts[1],
                    "avail": parts[2],
                })

            response = {
                "load": {
                    "1m": load_values[0],
                    "5m": load_values[1],
                    "15m": load_values[2],
                },
                "uptime": uptime_text,
                "zpool": zpool,
                "datasets": datasets
            }

            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps(response).encode())

        else:
            self.send_response(404)
            self.end_headers()


if __name__ == "__main__":
    server = HTTPServer((HOST, PORT), Handler)
    server.serve_forever()

Відразу скрипт для rc.d/usr/local/etc/rc.d/glance_api:

#!/bin/sh

# PROVIDE: glance_api
# REQUIRE: NETWORKING
# KEYWORD: shutdown

. /etc/rc.subr

name="glance_api"
rcvar="glance_api_enable"

command="/usr/local/bin/python3.11"
command_args="/usr/local/bin/glance_api.py"
pidfile="/var/run/${name}.pid"

start_cmd="${name}_start"
stop_cmd="${name}_stop"

glance_api_start()
{
    echo "Starting glance_api..."
    daemon -p ${pidfile} ${command} ${command_args}
}

glance_api_stop()
{
    echo "Stopping glance_api..."
    if [ -f ${pidfile} ]; then
        kill `cat ${pidfile}`
        rm -f ${pidfile}
    fi
}

load_rc_config $name
: ${glance_api_enable:=no}

run_rc_command "$1"

Запускаємо:

root@setevoy-nas:~ # chmod +x /usr/local/etc/rc.d/glance_api
root@setevoy-nas:~ # sysrc glance_api_enable=YES
glance_api_enable:  -> YES

root@setevoy-nas:~ # service glance_api start
Starting glance_api...

В Glance додаємо ще один блок:

...
          - type: custom-api
            title: NAS Status
            url: http://nas.setevoy:9001/status
            template: |
              <div>
                <div><b>Uptime:</b> {{ .JSON.String "uptime" }}</div>

                <div style="margin-top:10px;">
                  <b>Load:</b>
                  {{ .JSON.String "load.1m" }} /
                  {{ .JSON.String "load.5m" }} /
                  {{ .JSON.String "load.15m" }}
                </div>

                <div style="margin-top:10px;">
                  <b>ZFS:</b>
                  <span class="color-positive">
                    {{ .JSON.String "zpool.health" }}
                  </span>
                  ({{ .JSON.String "zpool.capacity" }} used)
                </div>

                <div style="margin-top:10px;">
                  <b>Datasets:</b>
                  <ul class="list list-gap-5">
                  {{ range .JSON.Array "datasets" }}
                    <li>
                      {{ .String "name" }} —
                      {{ .String "used" }} used
                    </li>
                  {{ end }}
                  </ul>
                </div>
              </div>
...

І тепер NAS Page виглядає так:

Можна було б в glance_api.py додати і виконання дій через POST – але я не став заморачуватись, та і виконувати команди з дашборди – це вже трохи занадто.

Останній штрих – Chrome/Firefox extension Custom New Tab URL:

Ну а потім, яе Glance переїде на окремий хост – то замінити URL.

Єдиний мінус в Glance – що він не вміє в auto refresh 🙁 Але можна зробити теж через екстешени браузеру.

Loading

FreeBSD: Home NAS, part 11 – extended моніторинг з додатковими експортерами
0 (0)

9 Лютого 2026

В попередньому пості FreeBSD: Home NAS, part 10 – моніторинг з VictoriaMetrics та Grafana налаштували VictoriaMetrics, node_exporter, Grafana, зробили базовий дашборд і базові алерти.

Тепер до цього хочеться додати  трохи більше моніторингу – бачити дані по CPU/RAM процесів, інформацію по SMART та ZFS.

Все, що тут написав – додав в репозиторій setevoy2/nas-monitoring: там і скрипти, і Grafana dashboard.

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

Установка кастомних експортерів

Не всі експортери мають порти або є в репозиторії – тому опишу, як зробив власний “pseudo FreeBSD port”.

Установка ZFS exporter

Репозиторій експортеру – zfs_exporter.

Взагалі в портах є інший експортер – py-prometheus-zfs, але я вже зробив з цим, і вийшло наче непогане – і цікаве – рішення, тому збережу в блог те, як це налаштував.

І те саме рішення використав для go-ecoflow-exporter.

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

Зато у всіх є код, і більшість сервісі на Go – тому їх легко зібрати самому.

Ідея доволі проста:

  • скрипт build.sh: завантажити або оновити код експортеру
  • Makefile: для запуску build.sh та копіювання файлу самого експортеру і його rc.d скрипта

Створюємо структуру каталогів:

# mkdir -p /opt/exporters/zfs_exporter/{rc.d,src}

Створення build.sh

Додаємо скрипт – він буде клонити репозиторій і виконувати go build.

Не став заморачуватись з файлом VERSION – просто беремо master бранч, і білдимо з нього.

Пишемо /opt/exporters/zfs_exporter/build.sh:

#!/bin/sh

# stop on first error
set -e

BASE_DIR="/opt/exporters/zfs_exporter"
SRC_DIR="${BASE_DIR}/src/zfs_exporter"
BIN_NAME="zfs_exporter"
REPO_URL="https://github.com/pdf/zfs_exporter.git"

# ensure src dir exists
mkdir -p "${BASE_DIR}/src"

# clone repo if it does not exist
if [ ! -d "${SRC_DIR}" ]; then
    git clone "${REPO_URL}" "${SRC_DIR}"
fi

cd "${SRC_DIR}"

# always update sources
git pull

# build binary into BASE_DIR
go build -o "${BASE_DIR}/${BIN_NAME}"

Задаємо права на запуск:

# chmod +x /opt/exporters/zfs_exporter/build.sh

Запускаємо:

# /opt/exporters/zfs_exporter/build.sh

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

# ll /opt/exporters/zfs_exporter/src/
total 8
drwxr-xr-x  6 root setevoy  512B Feb  9 13:32 zfs_exporter

І бінарний файл:

# file /opt/exporters/zfs_exporter/zfs_exporter 
/opt/exporters/zfs_exporter/zfs_exporter: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD) ...

Можна запустити його для перевірки:

# /opt/exporters/zfs_exporter/zfs_exporter
time=2026-02-09T13:42:30.119+02:00 level=INFO source=zfs_exporter.go:40 msg="Starting zfs_exporter" version="(version=, branch=, revision=7af698c8844864eb1e724ed08c47e5a7b4bbcc53)"
time=2026-02-09T13:42:30.120+02:00 level=INFO source=zfs_exporter.go:41 msg="Build context" context="(go=go1.24.12, platform=freebsd/amd64, user=, date=, tags=unknown)"
...
time=2026-02-09T13:42:30.120+02:00 level=INFO source=tls_config.go:354 msg="Listening on" address=[::]:9134
...

Тепер додаємо Makefile, аби простіше буде робити установку і апдейти.

Створення Makefile

Вся логіка апдейту і білда буде в build.sh, а в Makefile просто викликаємо сам скрипт і виконуємо установку файлів в систему:

# simple makefile for zfs_exporter

PREFIX=/usr/local
BIN_NAME=zfs_exporter
BASE_DIR=/opt/exporters/zfs_exporter

.PHONY: build install clean

build:
  $(BASE_DIR)/build.sh

install:
  install -m 0755 $(BASE_DIR)/$(BIN_NAME) $(PREFIX)/bin/$(BIN_NAME)

clean:
  rm -f $(BASE_DIR)/$(BIN_NAME)

Можемо використовувати його як:

# cd /opt/exporters/zfs_exporter
# make build

// або
# make -C /opt/exporters/zfs_exporter build

// або
# make -f /opt/exporters/zfs_exporter/Makefile build

Тепер у нас така структура експортеру:

# tree /opt/exporters/zfs_exporter
/opt/exporters/zfs_exporter
├── Makefile
├── build.sh
├── rc.d
├── src
│   └── zfs_exporter
│       ├── CHANGELOG.md
│       ├── LICENSE
...
│       └── zfs_exporter.go
└── zfs_exporter

Запускаємо для перевірки make build:

# make build 
/opt/exporters/zfs_exporter/build.sh
Already up to date.

І make install:

# make install 
install -m 0755 /opt/exporters/zfs_exporter/zfs_exporter /usr/local/bin/zfs_exporter

Перевіряємо ще раз, вже з /usr/local/bin:

# /usr/local/bin/zfs_exporter
...
time=2026-02-09T13:44:59.651+02:00 level=INFO source=tls_config.go:354 msg="Listening on" address=[::]:9134
...

Створення rc.d скрипта

Пишемо файл /opt/exporters/zfs_exporter/rc.d/zfs_exporter:

#!/bin/sh

# PROVIDE: zfs_exporter
# REQUIRE: DAEMON
# KEYWORD: shutdown

. /etc/rc.subr

name="zfs_exporter"
rcvar="zfs_exporter_enable"

command="/usr/local/bin/zfs_exporter"
pidfile="/var/run/${name}.pid"

# defaults (override in rc.conf)
: ${zfs_exporter_enable:=no}
: ${zfs_exporter_listen_address:=":9134"}
: ${zfs_exporter_extra_flags:=""}
: ${zfs_exporter_log_file:="/var/log/zfs_exporter.log"}

start_cmd="${name}_start"
stop_cmd="${name}_stop"
status_cmd="${name}_status"

zfs_exporter_start()
{
    echo "Starting ${name}"
    /bin/sh -c "${command} --web.listen-address=${zfs_exporter_listen_address} ${zfs_exporter_extra_flags} > ${zfs_exporter_log_file} 2>&1 & echo \$! > ${pidfile}"
}

zfs_exporter_stop()
{
    if [ -f "${pidfile}" ]; then
        kill "$(cat ${pidfile})"
        rm -f "${pidfile}"
        echo "Stopped ${name}"
    else
        echo "${name} is not running"
    fi
}

zfs_exporter_status()
{
    if [ -f "${pidfile}" ] && kill -0 "$(cat ${pidfile})" 2>/dev/null; then
        echo "${name} is running (pid $(cat ${pidfile}))"
    else
        echo "${name} is not running"
        return 1
    fi
}

load_rc_config $name
run_rc_command "$1"

Задаємо права на запуск:

# chmod +x /opt/exporters/zfs_exporter/rc.d/zfs_exporter

Додаємо його копіювання до /usr/local/etc/rc.d в таргет install нашого Makefile:

# simple makefile for zfs_exporter

PREFIX=/usr/local
BIN_NAME=zfs_exporter
BASE_DIR=/opt/exporters/zfs_exporter
RC_DIR=$(PREFIX)/etc/rc.d

.PHONY: build install clean

build:
  $(BASE_DIR)/build.sh

install:
  install -m 0755 $(BASE_DIR)/$(BIN_NAME) $(PREFIX)/bin/$(BIN_NAME)
  install -m 0755 $(BASE_DIR)/rc.d/$(BIN_NAME) $(RC_DIR)/$(BIN_NAME)

clean:
  rm -f $(BASE_DIR)/$(BIN_NAME)

Запускаємо:

# make install 
install -m 0755 /opt/exporters/zfs_exporter/zfs_exporter /usr/local/bin/zfs_exporter
install -m 0755 /opt/exporters/zfs_exporter/rc.d/zfs_exporter /usr/local/etc/rc.d/zfs_exporter

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

# sysrc zfs_exporter_enable="YES"
zfs_exporter_enable:  -> YES

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

# service zfs_exporter start
Starting zfs_exporter

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

# service zfs_exporter status
zfs_exporter is running (pid 91712)

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

# tail -f /var/log/zfs_exporter.log 
time=2026-02-09T13:52:07.033+02:00 level=INFO source=zfs_exporter.go:40 msg="Starting zfs_exporter" version="(version=, branch=, revision=7af698c8844864eb1e724ed08c47e5a7b4bbcc53)"
...
time=2026-02-09T13:52:07.033+02:00 level=INFO source=tls_config.go:354 msg="Listening on" address=[::]:9134
...

І метрики:

# curl -s localhost:9134/metrics | grep zfs_ | head -5
# HELP zfs_dataset_available_bytes The amount of space in bytes available to the dataset and all its children.
# TYPE zfs_dataset_available_bytes gauge
zfs_dataset_available_bytes{name="nas",pool="nas",type="filesystem"} 2.723599372288e+12
zfs_dataset_available_bytes{name="nas/backups",pool="nas",type="filesystem"} 2.723599372288e+12
zfs_dataset_available_bytes{name="nas/media",pool="nas",type="filesystem"} 2.723599372288e+12

VMAgent описував в попередньому пості в частині Установка VMAgent – додаємо до нього збір метрик з нового експортеру.

Редагуємо /usr/local/etc/prometheus/prometheus.yml, додаємо новий таргет:

...
  - job_name: "zfs_exporter"
    static_configs:
      - targets:
          - "127.0.0.1:9134"
...

Рестартимо vmagent і перевіряємо метрики в VictoriaMetrics:

Exporter upgrade з make

Тут у нас просто кілька кроків:

# service zfs_exporter stop

# make -C /opt/exporters/zfs_exporter build

# make -C /opt/exporters/zfs_exporter install

# service zfs_exporter start

Без VERSION чи тегів або релізів – все максимально просто.

Можна додати до Makefile:

...

upgrade:
        service zfs_exporter stop || true
        $(MAKE) build
        $(MAKE) install
        service zfs_exporter start

Установка go-ecoflow-exporter

Аналогічно зробив для go-ecoflow-exporter, але тут є пару відмінностей, бо треба передати пачку змінних оточення.

Для цього в скрипті /opt/exporters/ecoflow_exporter/rc.d/ecoflow_exporter додаємо export:

...
ecoflow_exporter_start()
{
    echo "Starting ${name}"

    export \
        EXPORTER_TYPE \
        ECOFLOW_EMAIL \
        ECOFLOW_PASSWORD \
        ECOFLOW_DEVICES \
        MQTT_DEVICE_OFFLINE_THRESHOLD_SECONDS \
        DEBUG_ENABLED \
        METRIC_PREFIX \
        SCRAPING_INTERVAL \
        PROMETHEUS_ENABLED \
        PROMETHEUS_PORT

    /bin/sh -c "${command} --listen ${ecoflow_exporter_listen_address} ${ecoflow_exporter_extra_flags} > ${ecoflow_exporter_log_file} 2>&1 & echo \$! > ${pidfile}"
}
...

А значення – через /etc/rc.conf.d і файл /etc/rc.conf.d/ecoflow_exporter:

# cat /etc/rc.conf.d/ecoflow_exporter
ecoflow_exporter_enable="YES"

EXPORTER_TYPE="mqtt"
...
PROMETHEUS_PORT="2112"

Ім’я файлу в rc.conf.d має збігатись з іменем в rc.d, тобто:

# cat /usr/local/etc/rc.d/ecoflow_exporter | grep name=
name="ecoflow_exporter"

Додаємо новий таргет до VMAgent:

...
  - job_name: "ecoflow_exporter"
    static_configs:
      - targets:
          - "127.0.0.1:2112"
...

Установка smartctl_exporter

Він є в портах – smartctl_exporter і в репозиторії FreeBSD, тому просто встановлюємо з pkg:

# pkg install -y smartctl_exporter

Додаємо запуск:

# sysrc smartctl_exporter_enable="YES"

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

# service smartctl_exporter start

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

# curl -s 127.0.0.1:9633/metrics | grep smart | grep -v \# | head -5
smartctl_device{ata_additional_product_id="unknown",ata_version="ACS-4 T13/BSR INCITS 529 revision 5",device="ada0" ...

Додаємо VMAgent таргет:

...
  - job_name: smartctl_exporter
    static_configs:
      - targets:
          - 127.0.0.1:9633
...

Але smartctl_exporter по дефолту збирає інформацію тільки для /dev/ada* – а в мене ще є NVMe.

В rc.d скрипті експортеру є glob:

...
smartctl_exporter_devices (string):           Shell glob (like /dev/ada[0-9]) for all devices
...

Задаємо диски в /etc/rc..conf:

...
smartctl_exporter_devices="/dev/ada* /dev/nvme0"
...

NVMe просто ім’я явно, без glob, бо буде тягнути розділи типу /dev/nvme0ns1.

Node Exporter та Textfile

Скільки років користуюсь node_exporter в Kubernetes – не знав, що у нього є така цікава можливість як Textfile, є навіть цілі колекції, див. node-exporter-textfile-collector-scripts.

Що хотілось бачити у себе – це інформацію по CPU/RAM процесів, і спочатку думав просто взяти process_exporter, як це робив в Kubernetes (див. Kubernetes: моніторинг процесів з process-exporter).

Але process_exporter не працює з FreeBSD, бо він всю інформацію збирає з каталогу /proc, який включити можна – але все одно буде не Linux-proc.

Тому зробив інакше – через node_exporter та textfiles, через які збираю температуру CPU та інформацію по процесам.

Перевіряємо, що директорію, з якої директорі node_exporter читає файли з метриками:

root@setevoy-nas:~ # ps aux | grep node_exporter
nobody            2511   0.0  0.2   1264028  13012  -  S    Fri17       1:17.17 /usr/local/bin/node_exporter --web.listen-address=:9100 --collector.textfile.directory=/var/tmp/node_exporter

--collector.textfile.directory=/var/tmp/node_exporter – Окей, включено, можна додавати дані.

Скрипт для CPU temperature, CPU та RAM процесами

Пишемо скрипт /usr/local/bin/process_resources_exporter.sh:

#!/bin/sh

OUT="/var/tmp/node_exporter/process_resources.prom"

# reset file once
{
  echo "# HELP local_process_memory_bytes resident memory size per process"
  echo "# TYPE local_process_memory_bytes gauge"

  echo "# HELP local_process_cpu_percent cpu usage percent per process"
  echo "# TYPE local_process_cpu_percent gauge"

  echo "# HELP node_cpu_temperature_celsius CPU/system temperature via ACPI"
  echo "# TYPE node_cpu_temperature_celsius gauge"
} > "$OUT"

# ----
# top processes by memory (aggregate by process name)
# ----
ps -axo comm,rss | grep -vE '^(idle|pagezero|kernel)' | awk '
{
  gsub(/ /,"_",$1)
  mem[$1] += $2
}
END {
  for (p in mem)
    printf "local_process_memory_bytes{process=\"%s\"} %d\n", p, mem[p] * 1024
}
' | sort -k2 -nr | head -10 >> "$OUT"

# ----
# top processes by cpu (aggregate by process name)
# ----
ps -axo comm,%cpu | grep -vE '^(idle|pagezero|kernel)' | awk '
{
  gsub(/ /,"_",$1)
  cpu[$1] += $2
}
END {
  for (p in cpu)
    printf "local_process_cpu_percent{process=\"%s\"} %.2f\n", p, cpu[p]
}
' | sort -k2 -nr | head -10 >> "$OUT"

# ----
# cpu temperature via ACPI
# ----
TEMP=$(sysctl -n hw.acpi.thermal.tz0.temperature 2>/dev/null | tr -d 'C')

if [ -n "$TEMP" ]; then
  echo "node_cpu_temperature_celsius $TEMP" >> "$OUT"
fi

Запускаємо для тесту:

# chmod +x /usr/local/bin/process_resources_exporter.sh

# /usr/local/bin/process_resources_exporter.sh

Перевіряємо файл /var/tmp/node_exporter/process_resources.prom  з метриками:

# cat /var/tmp/node_exporter/process_resources.prom 
# HELP local_process_memory_bytes resident memory size per process
# TYPE local_process_memory_bytes gauge
# HELP local_process_cpu_percent cpu usage percent per process
# TYPE local_process_cpu_percent gauge
# HELP node_cpu_temperature_celsius CPU/system temperature via ACPI
# TYPE node_cpu_temperature_celsius gauge
local_process_memory_bytes{process="jellyfin"} 456441856
...
local_process_cpu_percent{process="syslogd"} 0.00
node_cpu_temperature_celsius 27.9

І експортер з нього імпортує метрики:

Додаємо скрипт в cron, раз на хвилину буде достатньо:

* * * * * /usr/local/bin/process_resources_exporter.sh

Скрипт для freebsd_update та pkg updates

Аналогічно можна додати інформацію по доступним апдейтам.

Скрипт /usr/local/bin/updates_exporter.sh:

#!/bin/sh

OUT="/var/tmp/node_exporter/updates.prom"

# header
{
  echo "# HELP node_freebsd_update_available FreeBSD base system updates available (1=yes, 0=no)"
  echo "# TYPE node_freebsd_update_available gauge"

  echo "# HELP node_pkg_updates_available Number of pkg updates available"
  echo "# TYPE node_pkg_updates_available gauge"
} > "$OUT"

# --------
# freebsd-update
# --------
FREEBSD_UPDATES=0

# freebsd-update fetch returns:
# - exit 0 even if no updates
# - but prints "No updates needed to update system"
if freebsd-update fetch | grep -q "No updates needed"; then
  FREEBSD_UPDATES=0
else
  FREEBSD_UPDATES=1
fi

echo "node_freebsd_update_available $FREEBSD_UPDATES" >> "$OUT"

# --------
# pkg updates
# --------
# pkg version -l "<" lists outdated packages
PKG_UPDATES=$(pkg version -l "<" 2>/dev/null | wc -l | tr -d ' ')

# fallback safety
PKG_UPDATES=${PKG_UPDATES:-0}

echo "node_pkg_updates_available $PKG_UPDATES" >> "$OUT"

Робимо chmod, додаємо в cron – але тут раз на годину, і перевіряємо метрики в VictoriaMetrics:

Скрипт для Services health

Тут перевіряємо, що сервіси запущені, якщо все ОК – пишемо в метрику service_up значення 1, якщо проблеми – то 0.

Скрипт /usr/local/bin/service_status_exporter.sh:

#!/bin/sh

DIR="/var/tmp/node_exporter"
OUT="$DIR/service_status.prom"
TMP="$DIR/service_status.prom.tmp"

# ------------
# helpers
# ------------

check_proc() {
  pgrep -f "$1" >/dev/null 2>&1
}

check_port() {
  host="$1"
  port="$2"
  nc -z "$host" "$port" >/dev/null 2>&1
}

# ----------------
# write metrics (atomic)
# ----------------

cat <<EOF > "$TMP"
# HELP service_up Service availability status (1 = up, 0 = down)
# TYPE service_up gauge
EOF

# jellyfin
if check_port 127.0.0.1 8096; then
  echo 'service_up{name="jellyfin"} 1' >> "$TMP"
else
  echo 'service_up{name="jellyfin"} 0' >> "$TMP"
fi

# filebrowser
if check_port 127.0.0.1 8080; then
  echo 'service_up{name="filebrowser"} 1' >> "$TMP"
else
  echo 'service_up{name="filebrowser"} 0' >> "$TMP"
fi

# grafana
if check_port 127.0.0.1 3000; then
  echo 'service_up{name="grafana"} 1' >> "$TMP"
else
  echo 'service_up{name="grafana"} 0' >> "$TMP"
fi

# victoria-metrics
if check_port 127.0.0.1 8428; then
  echo 'service_up{name="victoria-metrics"} 1' >> "$TMP"
else
  echo 'service_up{name="victoria-metrics"} 0' >> "$TMP"
fi

# sshd (port only)
if check_port 127.0.0.1 22; then
  echo 'service_up{name="sshd"} 1' >> "$TMP"
else
  echo 'service_up{name="sshd"} 0' >> "$TMP"
fi

# nfsd (process only)
if check_proc nfsd; then
  echo 'service_up{name="nfsd"} 1' >> "$TMP"
else
  echo 'service_up{name="nfsd"} 0' >> "$TMP"
fi

# atomic replace
mv "$TMP" "$OUT"

І метрики:

Grafana дашборд – нові графіки

А тепер це все додаємо в Grafana.

Графік CPU процесами:

topk(5, local_process_cpu_percent)

Аналогічно для пам’яті:

Статус дисків зі SMART:

smartctl_device_smart_status

Стан сервісів:

І все разом тепер:

Залишилось додати алерти – і, в принципі, моніторинг готовий.

Loading

FreeBSD: налаштування DragonFly Mail Agent для пошти root
0 (0)

8 Лютого 2026

В локальну пошту юзера root приходить багато листів від різних periodic задач.

Про perdiodic scripts трохи писав у FreeBSD: Home NAS, part 5 – ZFS pool, datasets, snapshots та моніторинг, тепер налаштувати пересилку пошти на зовнішню адресу.

Бо листи приходять кожного дня, а читати пошту локально незручно:

# mail -u root -H
Mail version 8.1 6/6/93.  Type ? for help.
"/var/mail/root": 58 messages 58 unread
...
 U 57 root@setevoy-nas      Sun Feb  8 03:19  46/1300  "setevoy-nas daily security run output"
 U 58 root@setevoy-nas      Sun Feb  8 03:19  99/3444  "setevoy-nas daily run output"

Аби їх отримувати на зовнішній ящик – додаємо Mail Transport Agent (MTA), який буде робити відправку на задану адресу.

Створення Google Mail App Passwords

Якщо включений 2FA – то використовуємо Google Mail App Passwords.

Пароль відобразить з пробілами – в конфігу DMA їх прибираємо:

Налаштування DragonFly Mail Agent

Документація – DragonFly Mail Agent (DMA) (FreeBSD Handbook) та DMA (Arch Linux Wiki).

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

# pkg install dma

Редагуємо файл /etc/dma/dma.conf:

SMARTHOST smtp.gmail.com
PORT 587

# SMTP authentication
AUTHPATH /etc/dma/auth.conf

SECURETRANSFER 
STARTTLS 

MASQUERADE [email protected]

Редагуємо /etc/dma/auth.conf – задаємо логін, хост і App password, який створили на початку (без пробілів):

[email protected]|smtp.gmail.com:mpd***sra

Задаємо права доступу – з групою mail:

# chown root:mail /etc/dma/auth.conf
chmod 640 /etc/dma/auth.conf

Перевіряємо відправку на звичайну адресу:

# echo "dma alias test" | mail [email protected]

І отримуємо листа:

Але якщо зараз відправити до юзера root, то лист не дійде з помилкою “The recipient address <root> is not a valid RFC 5321 address“.

Додаємо пошту рута до /etc/aliases:

...
root: [email protected]
...

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

# echo "dma alias test" | mail root

І отримуємо листа “To: root“:

І для перевірки можна запустити виконання всіх periodic:

# periodic daily

І отримуємо листи:

Готово.

Loading

FreeBSD: Home NAS, part 10 – моніторинг з VictoriaMetrics та Grafana
0 (0)

7 Лютого 2026

Нарешті добрався до моніторингу.

Цікаво запустити стандартний стек з VictoriaMetrics + Grafana + Alertmanager не у звичному Kubernetes з Helm-чарту, а просто на хості.

Але підхід той самий, з яким моніторяться сервіси в AWS/Kubernetes – на FreeBSD буде VictoriaMetrics для метрик, Grafana для візуалізації, VMAlert та Alertmanager для алертів.

Хотя в моніторингу моїх EcoFlow зробив алерти через Grafana Alerts, перший раз їх пробував – непогано. Хоча все ж стандартний підхід, коли всі Alert Rules описані в файлах, мені заходить більше.

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

Оскільки це маленький домашній NAS, до якого доступ тільки в локальній мережі з через VPN – то буду робити без FreeBSD Jails. З ними, може, буду знайомитись ближче іншим разом, бо за всі роки користування FreeBSD (з… 2007 року? десь так) – жодного разу в jails нічого не крутив.

Поїхали.

Установка VictoriaMetrics

VictoriaMetrics є в портах FreeBSD і в репозиторії, хоча порти дещо відрізняються від звичної схеми – далі подивимось ці нюанси.

З репозиторію FreeBSD встановлюємо саму VictoriaMetrics:

root@setevoy-nas:~ # pkg install -y victoria-metrics

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

root@setevoy-nas:~ # pkg info -l victoria-metrics | grep -E 'bin|rc.d'
        /usr/local/bin/victoria-metrics
        /usr/local/etc/rc.d/victoria-metrics

У файлі /usr/local/etc/rc.d/victoria-metrics маємо список флагів, які можемо передати через /etc/rc.conf:

root@setevoy-nas:~ # cat /usr/local/etc/rc.d/victoria-metrics | grep victoria_metrics
# PROVIDE: victoria_metrics
name="victoria_metrics"
rcvar="victoria_metrics_enable"
logfile="${logdir}/victoria_metrics.log"
victoria_metrics_args=${victoria_metrics_args-"--storageDataPath=/var/db/victoria-metrics --retentionPeriod=1 --httpListenAddr=:8428"}
victoria_metrics_user="victoria-metrics"

Дані будуть зберігатись в /var/db/victoria-metrics, потім треба буде додати його в бекапи.

Додаємо запуск сервісу до /etc/rc.conf:

root@setevoy-nas:~ # sysrc victoria_metrics_enable="yes"
victoria_metrics_enable:  -> yes

Запускаємо:

root@setevoy-nas:~ # service victoria-metrics start

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

root@setevoy-nas:~ # netstat -an | grep 8428
tcp4       0      0 *.8428                 *.*                    LISTEN

Відкриваємо в браузері – все працює:

Установка node_exporter

Аби побачити в VictoriaMetrics якісь метрики – встановлюємо теж звичний багатьом node_exporter:

root@setevoy-nas:~ # pkg install -y node_exporter

Додаємо його запуск:

root@setevoy-nas:~ # sysrc node_exporter_enable="yes"
node_exporter_enable:  -> yes

Запускаємо:

root@setevoy-nas:~ # service node_exporter start
Starting node_exporter.

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

root@setevoy-nas:~ # netstat -an | grep 9100
tcp46      0      0 *.9100                 *.*                    LISTEN

Установка VMAgent

Отут вже трохи відмінностей, бо окремого FreeBSD port під VMAgent нема – але є загальний пакет vmutils, який встановлює відразу кілька компонентів:

root@setevoy-nas:~ # pkg install -y vmutils

Перевіряємо, що він нам додав:

root@setevoy-nas:~ # pkg info -l vmutils | grep bin
        /usr/local/bin/vmagent
        /usr/local/bin/vmalert
        /usr/local/bin/vmauth
        /usr/local/bin/vmbackup
        /usr/local/bin/vmctl
        /usr/local/bin/vmrestore

Але з vmutils встановлюється тільки один rc.d скрипт, для самого vmagent:

root@setevoy-nas:~ # pkg info -l vmutils | grep rc.d
        /usr/local/etc/rc.d/vmagent

Тому далі для VMAlert будемо писати власний.

Додаємо vmagent в rc.cconf:

root@setevoy-nas:~ # sysrc vmagent_enable="yes"
vmagent_enable:  -> yes

Поки не запускаємо – налаштуємо збір метрик з node_exporter.

Перевіряємо опції, з якими vmagent запускається – шукаємо дефолтний файл конфігу:

root@setevoy-nas:~ # cat /usr/local/etc/rc.d/vmagent
#!/bin/sh
...
vmagent_args=${vmagent_args-"--remoteWrite.tmpDataPath=/var/db/vmagent --promscrape.config=/usr/local/etc/prometheus/prometheus.yml --remoteWrite.url=http://127.0.0.1:8428/api/v1/write --memory.allowedPercent=80"}
...

Додаємо job="node_exporter" до /usr/local/etc/prometheus/prometheus.yml:

global:
  scrape_interval: 15s

scrape_configs:

  - job_name: vmagent
    scrape_interval: 60s
    scrape_timeout: 30s
    metrics_path: "/metrics"
    static_configs:
    - targets:
      - 127.0.0.1:8429
      labels:
        project: vmagent

  - job_name: "node_exporter"
    static_configs:
      - targets:
          - "127.0.0.1:9100"

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

root@setevoy-nas:~ # service vmagent checkconfig; echo $?
0

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

root@setevoy-nas:~ # service vmagent start

Перевіряємо в VMAgent /targets, http://nas.setevoy:8429/targets:

І метрики в VictoriaMetrics:

Установка Grafana

Теж встановлюємо із репозиторію:

root@setevoy-nas:~ # pkg install -y grafana

Конфіг-файл – /usr/local/etc/grafana/grafana.ini.

Додаємо запуск:

root@setevoy-nas:~ # sysrc grafana_enable="yes"
grafana_enable:  -> yes

Запускаємо:

root@setevoy-nas:~ # service grafana start
Starting grafana.

Для тестів можна взяти готовий дашборд – Node Exporter Full.

VictoriaMetrics Grafana data source на FreeBSD

Додаємо datasource:

Але відразу зловив помилку:

Хоча 100% плагін підписаний, бо не вперше ж його використовую в робочих проектах:

Пробував додати allow_loading_unsigned_plugins до /usr/local/etc/grafana/grafana.ini:

...
allow_loading_unsigned_plugins = victoriametrics-metrics-datasource
...

Або встановити з Grafaca CLI (перший раз не користувався):

root@setevoy-nas:~ # grafana cli plugins install victoriametrics-metrics-datasource
Grafana-server Init Failed: Could not find config defaults, make sure homepath command line parameter is set or working directory is homepat

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

Ну і потім вже глянув логи Grafana:

...
logger=installer.fs t=2026-02-06T17:09:29.946038823+02:00 level=info msg="Downloaded and extracted victoriametrics-metrics-datasource v0.21.0 zip successfully to /var/db/grafana/plugins/victoriametrics-metrics-datasource"
logger=plugins.backend.start t=2026-02-06T17:09:30.466419686+02:00 level=error msg="Could not start plugin backend" pluginId=victoriametrics-metrics-datasource error="fork/exec /var/db/grafana/plugins/victoriametrics-metrics-datasource/victoriametrics_metrics_backend_plugin_freebsd_amd64: no such file or directory"
...

“victoriametrics_metrics_backend_plugin_freebsd_amd64: no such file or directory”

Oh, c’mon…

Не став розбиратись далі – просто можемо використати стандартний плагін Prometheus (але пізніше розробників VictoriaMetrics потім за цю проблему спитаю).

Власне, якщо не зраджує пам’ять – то раніше, коли власного плагіну для Grafana у VictoriaMetrics не було, то ми і використовували дефолтний Prometheus, який йде в комплекті до Grafana:

Додаємо новий data source:

Називаємо його victoria-metrics, задаємо URL:

І тепер все працює:

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

I use Arch ntfy.sh BTW.

Дуже прикольний і простий сервіс. Має web, має мобільну апку.  Може напишу якось про нього окремо, бо просто в захваті.

Можна зробити алерти через Telegram – можливо, потім додам і його, але зараз ntfy.sh вистачить з головою.

Отже, VMlert у нас вже є – він рахує правила, які ми йому задаємо, і шле в Alertmanager.

Установка Alertmanager

Теж з репозиторію FreeBSD:

root@setevoy-nas:~ # pkg install -y alertmanager

Додаємо запуск:

root@setevoy-nas:~ # sysrc alertmanager_enable="yes"
alertmanager_enable:  -> yes

Видаляємо дефолтний файл, бо там багато зайвого:

root@setevoy-nas:~ # mv /usr/local/etc/alertmanager/alertmanager.yml /usr/local/etc/alertmanager/alertmanager.yml-default

Пишемо свій конфіг /usr/local/etc/alertmanager/alertmanager.yml:

global:
  resolve_timeout: 5m

route:
  receiver: "ntfy"
  group_by: ["alertname"]
  group_wait: 10s
  group_interval: 5m
  repeat_interval: 4h

receivers:
  - name: "ntfy"
    webhook_configs:
      - url: "https://ntfy.sh/setevoy-alertmanager-alerts"
        http_config:
          authorization:
            type: Bearer
            credentials: "tk_v9c***f2p"
        send_resolved: true

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

root@setevoy-nas:~ # service alertmanager restart

Перевіряємо його дашборд – http://nas.setevoy:9093/#/alerts:

Установка VMAlert

Бінарнік vmalert вже встановлено з пакету vmutils, але для нього нема скриптів для rc.d:

root@setevoy-nas:~ # pkg info -l vmutils | grep rc.d
        /usr/local/etc/rc.d/vmagent
root@setevoy-nas:~ # pkg info -l vmutils | grep bin
        /usr/local/bin/vmagent
        /usr/local/bin/vmalert
        /usr/local/bin/vmauth
        /usr/local/bin/vmbackup
        /usr/local/bin/vmctl
        /usr/local/bin/vmrestore

Тому пишемо власний /usr/local/etc/rc.d/vmalert – він простий, вайбокодиться без проблем:

#!/bin/sh

# PROVIDE: vmalert
# REQUIRE: LOGIN
# KEYWORD: shutdown

. /etc/rc.subr

name="vmalert"
rcvar="vmalert_enable"

load_rc_config $name

: ${vmalert_enable:="NO"}
: ${vmalert_user:="victoria-metrics"}
: ${vmalert_args:="--datasource.url=http://127.0.0.1:8428 --notifier.url=http://127.0.0.1:9093 --rule=/usr/local/etc/vmalert/*.yml"}

pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
procname="/usr/local/bin/vmalert"

command_args="-f -p ${pidfile} ${procname} ${vmalert_args}"

start_cmd="vmalert_start"
stop_cmd="vmalert_stop"

vmalert_start()
{
  echo "Starting vmalert"
  ${command} ${command_args}
}

vmalert_stop()
{
  echo "Stopping vmalert"
  kill `cat ${pidfile}`
}

run_rc_command "$1"

Задаємо права на запуск:

# chmod +x /usr/local/etc/rc.d/vmalert

Додаємо в запуск із системою:

root@setevoy-nas:~ # sysrc vmalert_enable="yes"
vmalert_enable:  -> yes

І запускаємо:

root@setevoy-nas:~ # service vmalert start

Перевіряємо на http://nas.setevoy:8880:

Додавання алертів

Власне, тепер можемо накидати алерти.

Створюємо файл /usr/local/etc/vmalert/node-alerts.yml:

root@setevoy-nas:~ # mkdir -p /usr/local/etc/vmalert/

Описуємо алерт:

groups:
  - name: node-exporter-alerts
    rules:
      - alert: NodeExporterDown
        expr: up{job="node_exporter"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "node_exporter down on {{ $labels.instance }}"
          description: "node_exporter is not reachable for more than 1 minute"

Перезапускаємо vmalert:

root@setevoy-nas:~ # service vmalert restart

Для тесту зупиняємо node_exporter:

root@setevoy-nas:~ # service node_exporter stop
Stopping node_exporter.
Waiting for PIDS: 1965.

І отримуємо алерт в VMAlert:

І повідомлення від ntfy.sh.

На телефоні:

Або в web:

Тюнинг Grafana dashboard та node_exporter Memory graphs

Дефолтна дашборда заточена під Linux, і на FreeBSD для правильного відображення графіків у Memory Basic треба трохи затюнити запити і метрики.

Перевіряємо наявні метрики по memory від node_exporter:

{__name__=~"node_memory_.*_bytes"}

Тут з основного маємо:

  • node_memory_size_bytes: total RAM
  • node_memory_free_bytes: реально вільна
  • node_memory_cache_bytes: filesystem cache (reclaimable)
  • node_memory_buffer_bytes: buffers (reclaimable)
  • node_memory_inactive_bytes: inactive pages (reclaimable)
  • node_memory_active_bytes: активно використовується
  • node_memory_wired_bytes: non-reclaimed памʼять (kernel, drivers)

Що нам тут цікаво – це загальна пам’ять, node_memory_free_bytes та node_memory_active_bytes.

Free RAM у FreeBSD – це дійсно free memory, тобто все поза кешами, inactive, wired, buffers і т.д.

Тому панельку з візуалізацією можемо побудувати з такими запитами:

  • загальна пам’ять
node_memory_size_bytes
  • used memory – те, що дійсно зайнято  і не може бути звільнено:
sum
(
    node_memory_active_bytes
    +
    node_memory_wired_bytes
)
  • вільна пам’ять – все поза всякими кешами-буферами і рештою зайнятої пам’яті:
node_memory_free_bytes{instance="$node",job="$job"}
  • swap used, хоча в мене його нема:
node_memory_swap_used_bytes{instance="$node",job="$job"}
  • відобразити % зайнятої памяті від загальної:
(
  node_memory_active_bytes
  + node_memory_wired_bytes
)
/
node_memory_size_bytes
* 100

В результаті маємо такі графіки для пам’яті:

А вся дашборда в мене поки виглядає так (це вже з доданим ZFS Exporter):

Окремо є Small варіант – для виводу на 7-ми дюймовий монітор, який буде стояти в серверній шафі:

Пізніше до дашборду вже пододаю ще корисних графіків і статусів.

Ну і для моніторингу системи і NAS буде корисно додати ще експортерів – smartctl_exporter, zfs_exporter тощо.

Loading

MikroTik: перше знайомство та Getting Started
0 (0)

22 Січня 2026

Давно подумував спробувати MikroTik, але все якось було ліньки розбиратись з RouterOS.

Нарешті, на хвилі зборки Home NAS проекту (див. початок в FreeBSD: Home NAS, part 1 – налаштування ZFS mirror) таки вирішив, що пора оновити і мережевий стек і замінити простенький TP-Link Archer на щось більш серйозне.

Так в мене з’явились два роутери MikroTik: RB4011iGS+RM – основний роутер, і MikroTik hAP ax3 – для WiFi.

До цього в мене були Linksys E4200 (2012-2020), потім з’явився Linksys EA6350 (2020-2024), і останнім був TP-Link Archer AX12 (2024-2025), і коли я перший раз відкрив MikroTik Web UI і подивився можливості… Це як пересісти з Запорожця на Мерседес.

Ну і нарешті – повноцінна консоль і SSH з коробки, а не через кастомні прошивки.

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

Архітектура моєї мережі

Перш ніж говорити про сам роутер – трохи про мій networking і ролі роутерів MikroTik.

Є дві мережі – “офіс” та дом, в обох “на вході” були TP-Link Archer AX12.

В “офісі” (в лапках, бо це просто сусідня квартира) стоїть ThnikCenter з FreeBSD/NAS, плюс робочий ноутбук і ігровий ПК. В основному всі девайси підключені до роутера кабелями, WiFi тільки для телефону і всяких EcoFlow, робота-пилососа тощо.

Вдома – пара ноутбуків, там вся мережа виключно WiFi.

Обидві мережі об’єднані з VPN, і по старій схемі TP-Link Archer в офісі мав port forwading до WireGuard на FreeBSD, а на FreeBSD були власне WireGuard як хаб та Unbound для локального DNS, плюс всякі Samba/NFS/etc.

Тепер жеж в офісі схема буде іншою:

  • MikroTik RB4011iGS:
    • на нього заходить кабель провайдера (оптика через ONU і далі по квартирі з Ethernet на роутер RB4011)
    • на ньому пізніше буде друге підключення ще одного LTE-роутера з SIM та мобільним інтернетом і автоматичним failover (див. старий пост Networking: коли немає світла – модем 4G ZTE + зовнішня антена: антена та сама, тільки роутер буде Teltonika RUT241)
    • WireGuard тепер буде тут
    • локальний DNS теж тепер буде тут
    • до RB4011iGS кабелями підключені ThinkCentre/NAS, робочий ноутбук та ігровий ПК
  • MikroTik hAP ax3: підключений кабелем до RB4011, пізніше переключу його в режим Access Point, поки стандартний WiFi роутер з власним NAT
  • TP-Link Archer AX12: підключений кабелем до RB4011, на ньому нічого не міняю, бо ліньки перепідключати різні домашні девайси типу дверного дзвоника, пожежної сигналізації, EcoFlow, etc

В домашній мережі не міняється нічого окрім налаштувань WireGuard на домашньому ноуті: раніше він через port-forwarding на офісному роутері підключався до FreeBSD, тепер буде ходити до RB4011.

І окремо – сервер самого rtfm.co.ua в DigitalOcean, який (буде) теж підключений з WireGuard до цієї мережі.

Загальна схема буде виглядати приблизно так:

Перше підключення до MikroTik

Боже, який це кайф – мати повноцінний SSH! Але про SSH трохи далі тут і потім ще окремим постом.

Взагалі MikroTik предоставляє кілька варіантів підключення:

Дефолтний юзер всюди admin. Паролі для MikroTik hAP ax3 зробили на висувному вкладиші (дуже прикольно):

А для RB4011 на паперовому Quick Guide.

Дефолтна мережа 192.168.88.0/24, адреса роутера, відповідно – 192.168.88.1.

WAN-порт на обох роутерах – перший, в нього втикаємо кабель провайдера, а ноутбук/ПК – в будь-який інший.

Для підключення до MikroTik hAP ax3 замість кабеля можна використати його дефолтну WiFi-мережу – теж дасть доступ до управління.

Web UI overview

Інтерфейс стандартний на обох роутерах – на RB4011 навіть є розділ “WiFi”, хоча в нього тільки Ethernet-порти.

Тут і далі в основному буду писати про RB4011, тому скріншоти будуть з нього.

Web має три “режими” – простий для Quick Setup:

Advanced – тут вже доступ до всіх можливостей:

І можна запустити Terminal прямо з Web:

В Design Skin можна вибрати які саме пункти меню будуть відображатись:

WinBox

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

При цьому підключитись можна як по IP, так і по MAC-адресі – на випадок, якщо зламали мережу.

А сам інтерфейс в принципі такий самий, як і в Web – стандартний для RouterOS:

І навіть є темна тема:

SSH

Тут все стандартно – просто з ноутбука/ПК виконуємо “ssh 192.168.88.1” (в мене вже переналаштований DHCP, тому адреса 192.168.0.1):

Можна використовувати ключі замість паролів, потім подивимось як.

Mobile client

І мобільна апка – підключаємось по IP:

Робота в консолі RouterOS

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

Документація – Command Line Interface та Console.

Дуже цікава можливість Safe Mode: відкотить зміни, якщо зламали доступ і підключення розірвалось без коректного збереження налаштувань.

В RouteOS є повноцінна консоль, яка складається з ієрархічного дерева команд.

Наприклад, якщо в Web пункт меню IP => Firewall:

То в консолі це буде /ip firewall.

Є повноцінне автодоповнення по Tab:

Після переходу в меню можна з F1 подивитись доступні команди:

В документації говориться, що “?” має виводити підказку теж – але на 7+ версії це вже не працює (Reddit).

Замість “?” просто вибираємо команду, а потім F1 або Tab:

Getting started: перші налаштування

У MikroTik дуже класна документація (привіт, Confluence), і є власний розділ Getting started.

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

Деякі скріни старі, тому ім’я хоста там буде “MikroTik” – дефолтне, далі глянемо, як це міняється.

IP теж може бути старий, дефолтний – 192.168.88.1. зараз він 192.168.0.1. Про налаштування DHCP – в наступному пості.

Backup та restore

У MirkoTik є два варіанти створення бекапу – з /export та /system backup.

/export створить текстовий файл з історією команд який можна прочитати – а /system backup створює бінарний файл, який включає в себе все, в тому числі ключі і сертифікати.

Але якщо конфіг переноситься на інший роутер – то system backup може сфейлитись, бо містить в собі прив’язку до конкретного девайсу, а результат з export – просто виконає команди.

/export та /import

Робимо /export в файл:

[setevoy@mikrotik-rb4011-gw] > /export file=init-backup

Тепер він є в Files:

І його можна прочитати.

З scp копіюємо собі на ноут:

[setevoy@setevoy-work ~] $ scp [email protected]:/init-backup.rsc .
[email protected]'s password: 
init-backup.rsc

І читаємо:

[setevoy@setevoy-work ~]  $ cat init-backup.rsc 
# 2026-01-22 15:21:51 by RouterOS 7.21
# software id = BUXG-TCU3
#
# model = RB4011iGS+
# serial number = HK50AXX5M2Y
/interface bridge
add admin-mac=04:F4:1C:89:8B:B3 auto-mac=no comment=defconf name=bridge
/interface wireguard
add listen-port=51820 mtu=1420 name=wg0
/interface list
add comment=defconf name=WAN
add comment=defconf name=LAN
/ip pool
add name=default-dhcp ranges=192.168.88.10-192.168.88.254
add name=vpn ranges=192.168.89.2-192.168.89.255
add name=dhcp_pool_lan ranges=192.168.0.50-192.168.0.200
...

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

/import file-name=init-backup.rsc

Система просто прочитає всі команди по черзі і виконає їх.

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

Експорт/імпорт не відновить:

  • паролі
  • сертифікати і приватні ключі
  • ліцензію
  • secrets (IPsec, WireGuard private keys)
  • деякі параметри /system

/system backup save та load

Для створення повного бекапу:

/system backup save name=before-change

Для відновлення:

/system backup load name=before-change

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

User management

Рекомендується створити власного юзера з root-правами, і відключити(але не видаляти) дефолтного admin.

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

Першим ділом міняємо пароль admin:

[admin@mikrotik-rb4011-gw] > /user set admin password=PASSWORD

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

Вивести всіх юзерів :

/user print

Активні сесії:

/user active print

Створюємо юзера, і задамо ліміт на адреси, з яких буде доступ (хоча ремоут SSH по дефолту і так недоступний на фаєрволі, про нього згодом):

/user add name=setevoy group=full password=PASSWORD address=192.168.0.0/24,192.168.88.0/24

Перевіряємо – або для всіх з /user print detail, або конкретного з where:

/user print detail where name="setevoy"

Змінити пароль або інші атрибути:

/user set [find name="setevoy"] password=NEW_PASSWORD

Або по ID – знаходимо ID з /user print:

І використовуємо його для /user set:

/user set 1 password=NEW_PASSWORD

Підключаємось під новим юзером:

[setevoy@setevoy-work ~]  $ ssh 192.168.0.1
...
[email protected]'s password: 
...
[setevoy@mikrotik-rb4011-gw] >

Апгрейд роутера

Бекапимось 🙂

Хоча завжди можна резетнути до заводських налаштувань, але краще завести звичку створити бекап.

Апгрейд включає в себе два окремих процеси – оновлення RouterOS та апдейти для firmware.

RouterOS upgrade

Документація – Upgrading and installation.

Перевіряємо поточну версію системи:

/system package print

Результат:

Columns: NAME, VERSION, BUILD-TIME, SIZE
# NAME      VERSION  BUILD-TIME           SIZE   
0 routeros  7.18.2   2025-03-11 11:59:04  11.5MiB

Перевіряємо наявність апдейтів:

/system package update check-for-updates

Результат:

[setevoy@MikroTik] > /system package update check-for-updates
            channel: stable                  
  installed-version: 7.18.2                  
     latest-version: 7.21                    
             status: New version is available

Завантажуємо – це тільки загрузка:

/system package update download

Результат:

[setevoy@MikroTik] > /system package update download
            channel: stable                                        
  installed-version: 7.18.2                                        
     latest-version: 7.21                                          
             status: Downloaded, please reboot router to upgrade it

І запускаємо вже сам процес апгрейду:

/system package update install

Система піде в ребут:

[setevoy@MikroTik] > /system package update install
            channel: stable                      
  installed-version: 7.18.2                      
     latest-version: 7.21                        
             status: calculating download size...
Received disconnect from 192.168.88.1 port 22:11: shutdown/reboot
Disconnected from 192.168.88.1 port 22

RouterBOARD (firmware) upgrade

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

Перевіряємо поточну версію:

/system routerboard print

В мене це виглядало так:

[setevoy@MikroTik] > /system routerboard print
       routerboard: yes        
             model: RB4011iGS+ 
          revision: r2         
     serial-number: HK50AXX5M2Y
     firmware-type: al2        
  factory-firmware: 7.18.2     
  current-firmware: 7.18.2     
  upgrade-firmware: 7.21

Встановлена 7.18.2, є апгрейд до 7.21.

Запускаємо апгрейд:

/system routerboard upgrade

Результат:

[setevoy@MikroTik] > /system routerboard upgrade
Do you really want to upgrade firmware? [y/n] 
y
[setevoy@MikroTik] > 
14:13:58 echo: system,info,critical Firmware upgraded successfully, please reboot for changes to take effect!

Ребутаємо роутер:

[setevoy@MikroTik] > /system reboot
Reboot, yes? [y/N]: 
y
system will reboot shortly
Connection to 192.168.88.1 closed.

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

[setevoy@MikroTik] > /system routerboard print  
       routerboard: yes        
             model: RB4011iGS+ 
          revision: r2         
     serial-number: HK50AXX5M2Y
     firmware-type: al2        
  factory-firmware: 7.18.2     
  current-firmware: 7.21       
  upgrade-firmware: 7.21

System management: основні команди

Корисні команди для роботи з системою.

Вивести події з логу:

/log print

Або з фільтром:

/log print where topics~"error|warning"

Вивести стан системи, версію, аптайм:

/system resource print

Коректно вимкнути систему:

/system shutdown

Перевірка живлення, температури:

/system health print

Навантаження CPU:

/tool profile

Cтан інтерфейсів коротко:

/interface print

Або детально:

/interface print detail

Адреси:

/ip address print

Роути:

/ip route print

Distance тут – пріорітет: можна мати друге інтернет-підключення (як я планую – на ethernet port 2 підключити LTE-роутер з SIM-картою), задати йому Distance == 2, і тоді трафік буде йти через перший порт – якщо він доступний, а якщо ні – то через другий.

Інформація по DNS:

/ip dns print

Виконати ping на якийсь хост:

/ping 8.8.8.8 src-address=192.168.0.1

Або traceroute (динамічний, як mtr на Linux/FreeBSD):

/tool traceroute 8.8.8.8

Коректно перезавантажити або виключити:

/system reboot
/system shutdown

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

/system identity set name=mikrotik-rb4011-gw

Власне, на цьому для початку все.

Що далі? Next steps

Про що ще думаю писати – частина вже є в чернетках, частину буду (якщо буде час) писати з нуля:

  1. налаштування DHCP
  2. налаштування DNS
  3. SSH і firewall – юзери, аутентифікація по ключам, правила фаєрволу
  4. налаштування WireGuard для підключення Peers
  5. scripts, alerting, monitoring – дуже класна можливість писати скрипти, які можуть слати алерти, див. Scripting
  6. резервний канал інтернету через LTE-роутер
  7. WiFi tuning

Loading

FreeBSD: Home NAS, part 9 – backup даних з rclone до AWS S3 та Google Drive
0 (0)

21 Січня 2026

В попередньому пості серії по налаштуванню Home NAS на FreeBSD знайомились з Restic – утилітою для роботи з бекапами, і яка підтримує шифрування, снапшоти, історію змін, див. FreeBSD: Home NAS, part 8 – backup даних NFS та Samba з restic.

Але окрім архівних даних в S3 хочеться мати “offsite hot copy” в Google Drive та AWS S3, аби мати доступ до даних постійно, і які не треба відновлювати із бекапу, а можна просто скопіювати з CLI або навіть з браузера.

При цьому не хочеться розводити зоопарк різних систем, а працювати з одною, яка буде вміти підключатись і до AWS, і до Google Drive.

Власне, під час пошуку того, як з restic копіювати дані в Google Drive знайшов такий собі “швейцарський ніж” – Rclone.

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

rclone overview

Rclone (“rsync for cloud storage“) – CLI-утиліта, вміє працювати просто з безліччю різних бекендів – і локальними даними або NFS, Samba, і FTP, і WebDAV, і, звісно, AWS S3 та Google Drive, див. всі в Overview of cloud storage systems.

Основні плюшки системи:

  • написаний на Go
  • можливість одною CLI отримати доступ до даних в Google Drive та S3
  • client-side шифрування даних і імен файлів
  • режими copy та sync, аналогічні rsync
  • є можливість змонтувати ремоут в локальну директорію, і працювати як зі звичайною папкою з даними (див. rclone mount)
  • вміє працювати як “проксі” між двома remotes (наприклад, копіювати дані між Google Drive та S3)
  • має Web GUI

Але при цьому rclone не є саме бекап-системою – не використовує снапшоти, не веде історію змін даних, не відновлює стан “на дату”.

rclone та Google Drive backend

Почнемо з Google Drive, бо це основне, для чого я планую використовувати rclone, але далі налаштуємо і AWS S3.

Документація – Google Drive.

Створення Google API keys для rclone

Для роботи з Google Drive створимо API keys, і цей процес опишу окремо, бо створення ключів в Google трохи заплутане, і я кожного разу шукаю гайд як це робити.

Переходимо в Google API Console, вибираємо існуючий або створюємо новий проект:

Зліва вибираємо “Enabled APIs & services”, клікаємо “Enable APIs”:

В пошуку знаходимо “Google Drive API”:

Вмикаємо його:

 

Переходимо в “OAuth consent screen”:

Переходимо в “Branding”, заповнюємо “App information” – задаємо ім’я, це чисто для нас, вказуємо email:

І внизу в “Developer contact information” ще раз пошту:

 

Зберігаємо, переходимо в “Audience”, перевіряємо, що тут “User type” заданий як External:

Переходимо в “Clients”, потім “Create client”:

Вказуємо “Application type” як Desktop app:

Отримуємо Client ID та Client Secret, зберігаємо собі:

Переходимо до налаштувань підключень вже в самому rclone.

Налаштування Google Drive remote

Виконуємо rclone config, вибираємо “n) New remote“, задаємо ім’я:

...
e/n/d/r/c/s/q> n

Enter name for new remote.
name> nas-google-drive
...

Далі вибираємо з яким бекендом rclone буде працювати.

Для Google Drive це 22 (можна вказати номер, можна ім’я “drive“):

...
22 / Google Drive
   \ (drive)
...

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

...
Option client_id.
Google Application Client Id
...
Enter a value. Press Enter to leave empty.
client_id> 377***7i7.apps.googleusercontent.com

Option client_secret.
OAuth Client Secret.
Leave blank normally.
Enter a value. Press Enter to leave empty.
client_secret> GOC***gjX
...

Задаємо рівень доступу – тут можна дати або повний доступ до всього драйву, або, якщо rclone буде тільки для бекапів, то вибрати “Access to files created by rclone only”.

На ноутбуках можна задати повний доступ, а на FreeBSD зробимо “тільки для свої файлів”:

В Advanced можна редагувати параметри типу “use_trash” та “Upload chunk size”, але це можна зробити пізніше – зараз просто тиснемо Enter.

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

...
Use web browser to automatically authenticate rclone with remote?
 * Say Y if the machine running rclone has a web browser you can use
 * Say N if running rclone on a (remote) machine without web browser access
If not sure try Y. If Y failed, try N.

y) Yes (default)
n) No

Так як це робиться на FreeBSD без браузеру, то вибираємо No – rclone згенерує токен, який треба вказати на машині з браузером, де є інший інстанс rclone:

...
y/n> n

Option config_token.
For this to work, you will need rclone available on a machine that has
a web browser available.
For more help and alternate methods see: https://rclone.org/remote_setup/
Execute the following on the machine with the web browser (same rclone
version recommended):
        rclone authorize "drive" "eyJ***ifQ"
Then paste the result.
Enter a value.

Виконуємо на ноуті:

$ rclone authorize "drive" "eyJ***ifQ"
2026/01/07 16:35:38 NOTICE: Make sure your Redirect URL is set to "http://127.0.0.1:53682/" in your custom config.
2026/01/07 16:35:38 NOTICE: If your browser doesn't open automatically go to the following link: http://127.0.0.1:53682/auth?state=Lf9q_HUVFlBUc2UqlSUqpw
2026/01/07 16:35:38 NOTICE: Log in and authorize rclone for access
2026/01/07 16:35:38 NOTICE: Waiting for code...

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

Дозволяємо доступ:

Отримуємо Success:

А на ноут в консолі, з якої викликали rclone authorize прийде токен:

...
2026/01/07 16:35:38 NOTICE: Waiting for code...
2026/01/07 16:37:33 NOTICE: Got code
Paste the following into your remote machine --->
eyJ...ifQ
<---End paste

Копіюємо його до rclone config на FreeBSD хості:

...
Enter a value.
config_token> eyJ***ifQ
...

І нове підключення готове:

...
Configuration complete.
Options:
- type: drive
- client_id: 377***7i7.apps.googleusercontent.com
- client_secret: GOC***gjX
- scope: drive.file
- token: {"access_token":"ya2***","expires_in":3599}
- team_drive: 
Keep this "nas-google-drive" remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote

Глянути всі налаштовані бекенди можна з rclone listremotes:

root@setevoy-nas:/home/setevoy # rclone listremotes
nas-google-drive:

Або повна інформація з rclone config show:

root@setevoy-nas:/home/setevoy # rclone config show
[nas-google-drive]
type = drive
client_id = ***7i7.apps.googleusercontent.com
client_secret = GOCSPX-***gjX
scope = drive.file
token = {"access_token":"???"expires_in":3599}
team_drive =

scope = drive.file тут як раз вказує доступ тільки до даних від самого rclone.

Перевіряємо доступ до диску – свторюємо каталог:

root@setevoy-nas:~ # rclone mkdir nas-google-drive:Backups/Rclone

Перевіряємо зміст з rclone lsd та -R (recursive):

root@setevoy-nas:~ # rclone lsd -R nas-google-drive:Backups
           0 2026-01-20 16:04:10        -1 Rclone

Тепер налаштуємо ще AWS S3, а далі подивимось на основні команди для роботи з rclone.

rclone та AWS S3 backend

Документація – Amazon S3 Storage Providers.

Якби rclone був десь в AWS на EC2 або в EKS – то можна було б використати IAM Role, зараз просто зробимо з ключами.

Створення AWS IAM Policy та IAM User

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

Створюємо бакет:

Створюємо IAM Policy з повним доступом тільки до цього бакету:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RcloneNasBackupsFullAccess",
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::setevoy-backups-nas",
        "arn:aws:s3:::setevoy-backups-nas/*"
      ]
    }
  ]
}

Зберігаємо:

Створюємо юзера – без доступу до AWS Management Console:

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

Зберігаємо юзера:

Створюємо для нього ключі доступу:

Вибираємо Application running outside AWS:

Зберігаємо ключі:

Налаштування AWS S3 remote

Запускаємо rclone config, вибираємо “New remote”, задаємо ім’я:

root@setevoy-nas:~ # rclone config
Current remotes:

Name                 Type
====                 ====
nas-google-drive     drive

e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q> n

Enter name for new remote.
name> nas-s3-setevoy-backups
...

Далі тип – вибираємо 4 (s3), потім 1 – AWS:

...
Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
 1 / 1Fichier
   \ (fichier)
 2 / Akamai NetStorage
   \ (netstorage)
 3 / Alias for an existing remote
   \ (alias)
 4 / Amazon S3 Compliant Storage Providers including AWS, Alibaba, ArvanCloud, Ceph, ChinaMobile, Cloudflare, DigitalOcean, Dreamhost, Exaba, FlashBlade, GCS, HuaweiOBS, IBMCOS, IDrive, IONOS, LyveCloud, Leviia, Liara, Linode, Magalu, Mega, Minio, Netease, Outscale, OVHcloud, Petabox, RackCorp, Rclone, Scaleway, SeaweedFS, Selectel, StackPath, Storj, Synology, TencentCOS, Wasabi, Qiniu, Zata and others
   \ (s3)
...
Storage> s3

Option provider.
Choose your S3 provider.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / Amazon Web Services (AWS) S3
   \ (AWS)
...

Далі вибираємо “Enter AWS credentials in the next step” і задаємо ключі:

...
Option env_auth.
Get AWS credentials from runtime (environment variables or EC2/ECS meta data if no env vars).
Only applies if access_key_id and secret_access_key is blank.
Choose a number from below, or type in your own boolean value (true or false).
Press Enter for the default (false).
 1 / Enter AWS credentials in the next step.
   \ (false)
 2 / Get AWS credentials from the environment (env vars or IAM).
   \ (true)
env_auth> 1

Option access_key_id.
AWS Access Key ID.
Leave blank for anonymous access or runtime credentials.
Enter a value. Press Enter to leave empty.
access_key_id> AKI***VXZ

Option secret_access_key.
AWS Secret Access Key (password).
Leave blank for anonymous access or runtime credentials.
Enter a value. Press Enter to leave empty.
secret_access_key> MkP***xJ/
...

Далі регіон бакету – він є в Properties:

Задаємо його:

...
Option region.
Region to connect to.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
   / The default endpoint - a good choice if you are unsure.
 1 | US Region, Northern Virginia, or Pacific Northwest.
   | Leave location constraint empty.
   \ (us-east-1)
...
region> eu-west-1
...

Option endpoint залишаємо без мін, в Option location_constraint ще раз задаємо “eu-west-1“:

Option location_constraint.
Location constraint - must be set to match the Region.
Used when creating buckets only.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / Empty for US Region, Northern Virginia, or Pacific Northwest
   \ ()
...
 6 / EU (Ireland) Region
   \ (eu-west-1)
...
location_constraint> eu-west-1

Option acl можна пропустити – у нас окрема корзина, в якій вже є всі налаштування ACL.

В server_side_encryption вибираємо “AES256“, далі “None“:

...
Option server_side_encryption.
The server-side encryption algorithm used when storing this object in S3.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / None
   \ ()
 2 / AES256
   \ (AES256)
 3 / aws:kms
   \ (aws:kms)
server_side_encryption> 2

Option sse_kms_key_id.
If using KMS ID you must provide the ARN of Key.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / None
   \ ()
 2 / arn:aws:kms:*
   \ (arn:aws:kms:us-east-1:*)
sse_kms_key_id> 1

Далі тип Storage Classes, див The Ultimate Guide to AWS S3 Pricing in 2026.

Можна взяти INTELLIGENT_TIERING:

...
Option storage_class.
The storage class to use when storing new objects in S3.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / Default
   \ ()
...
 8 / Intelligent-Tiering storage class
   \ (INTELLIGENT_TIERING)
...
storage_class> 8

Зберігаємо – маємо новий бекенд:

...

Edit advanced config?
y) Yes
n) No (default)
y/n> 

Configuration complete.
Options:
- type: s3
- provider: AWS
- access_key_id: AKI***VXZ
- secret_access_key: MkP***zxJ/
- region: eu-west-1
- location_constraint: eu-west-1
- server_side_encryption: AES256
- storage_class: INTELLIGENT_TIERING
Keep this "nas-s3-setevoy-backups" remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d> 

Current remotes:

Name                 Type
====                 ====
nas-google-drive     drive
nas-s3-setevoy-backups s3
...

Для роботи з S3 бакетом використовуємо формат remote_name:backet_name.

Створюємо в бакеті файл healthcheck.txt і каталог test – використовуємо rclone rcat:

root@setevoy-nas:~ # echo test | rclone rcat nas-s3-setevoy-backups:setevoy-backups-nas/test/healthcheck.txt

Перевіряємо зміст корзини з rclone ls:

root@setevoy-nas:~ # rclone ls nas-s3-setevoy-backups:setevoy-backups-nas/test
        5 healthcheck.txt

Rclone та шифрування

Задля більшої безпеки rclone може шифрувати свій локальний конфігураційний файл, а для безпечного зберігання даних в remote backends – може шифрувати дані там.

Rclone remote Crypt backend

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

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

Наприклад, маючи nas-google-drive можна створити новий storage backend nas-google-drive-crypted і використовувати його: він буде таким собі “проксі” – ми пишемо дані “в нього”, він виконує шифрування, а потім “під капотом”, аби записати файли в Google Drive, використовує “оригінальний” бекенд nas-google-drive.

Створюємо новий remote:

root@setevoy-nas:~ # rclone config
Current remotes:

Name                 Type
====                 ====
nas-google-drive     drive
nas-s3-setevoy-backups s3

e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q> n

Enter name for new remote.
name> nas-google-drive-crypted

В типі вибираємо “15 – Encrypt/Decrypt a remote”:

...
Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
...
15 / Encrypt/Decrypt a remote
   \ (crypt)
...
Storage> crypt

Далі вказуємо “source backend”, в якому будуть зашифровані дані.

Тут важливий момент: можна вказати або весь storage як nas-google-drive:Backups/Rclone (корінь для rclone в моєму випадку) – або створити в ньому окремий каталог, в якому будуть зберігатись зашифровані дані:

...
Option remote.
Remote to encrypt/decrypt.
Normally should contain a ':' and a path, e.g. "myremote:path/to/dir",
"myremote:bucket" or maybe "myremote:" (not recommended).
Enter a value.
remote> nas-google-drive:Backups/Rclone/Vault

Далі є варіанти – шифрувати імена файлів і каталогів чи ні, дефолт – шифрувати:

...
Option filename_encryption.
How to encrypt the filenames.
Choose a number from below, or type in your own value of type string.
Press Enter for the default (standard).
   / Encrypt the filenames.
 1 | See the docs for the details.
   \ (standard)
 2 / Very simple filename obfuscation.
   \ (obfuscate)
   / Don't encrypt the file names.
 3 | Adds a ".bin", or "suffix" extension only.
   \ (off)
filename_encryption> 

Option directory_name_encryption.
Option to either encrypt directory names or leave them intact.
NB If filename_encryption is "off" then this option will do nothing.
Choose a number from below, or type in your own boolean value (true or false).
Press Enter for the default (true).
 1 / Encrypt directory names.
   \ (true)
 2 / Don't encrypt directory names, leave them intact.
   \ (false)
directory_name_encryption> 

І останнє – вказати пароль і опціонально salt:

...
Option password.
Password or pass phrase for encryption.
Choose an alternative below.
y) Yes, type in my own password
g) Generate random password
y/g> y
Enter the password:
password:
Confirm the password:
password:

Option password2.
Password or pass phrase for salt.
Optional but recommended.
Should be different to the previous password.
Choose an alternative below. Press Enter for the default (n).
y) Yes, type in my own password
g) Generate random password
n) No, leave this optional password blank (default)
y/g/n>

Готово:

...
Configuration complete.
Options:
- type: crypt
- remote: nas-google-drive:Backups/Rclone/Vault
- password: *** ENCRYPTED ***
Keep this "nas-google-drive-crypted" remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d> 

Current remotes:

Name                 Type
====                 ====
nas-google-drive     drive
nas-google-drive-crypted crypt
nas-s3-setevoy-backups s3

Тепер, якщо скопіюємо туди текстовий файл, то прочитати його зможемо тільки з rclone:

root@setevoy-nas:~ # rclone copy /root/rclone-copy.txt nas-google-drive-crypted:

Якщо глянути його в Google Drive, то побачимо щось таке:

Якщо просто напряму просто завантажити собі на ноутбук із Google Drive Web UI, то дані теж недоступні:

[setevoy@setevoy-work ~]  $ file Temp/56ncq9f6nnvup446abn28tno20 
Temp/56ncq9f6nnvup446abn28tno20: data

[setevoy@setevoy-work ~]  $ cat Temp/56ncq9f6nnvup446abn28tno20 
����~���ܻv� ���ڰ�~qz
����zG�4nNEQ

Але все нормально читається з rclone:

root@setevoy-nas:~ # rclone cat nas-google-drive-crypted:rclone-copy.txt
test

І можна відновити собі локально з rclone copy або rclone copyto:

root@setevoy-nas:~ # rclone copyto nas-google-drive-crypted:rclone-copy.txt /home/setevoy/decrypted-rclone-copy.txt

root@setevoy-nas:~ # cat /home/setevoy/decrypted-rclone-copy.txt
test

rclone та config file ecnryption

rclone зберігає всі свої налаштування в файлі ~/.config/rclone/rclone.conf, і по дефолту файл не шифрується:

root@setevoy-nas:~ # cat ~/.config/rclone/rclone.conf
[nas-google-drive]
type = drive
client_id = ***.apps.googleusercontent.com
client_secret = GOCSPX-***
scope = drive.file
token = {"access_token":***","expiry":"2026-01-17T18:09:01.116266823+02:00","expires_in":3599}
team_drive =

Але можна задати пароль:

root@setevoy-nas:~ # rclone config encryption set
Enter NEW configuration password:
password:
Confirm NEW configuration password:
password:

Тепер файл зашифровано:

root@setevoy-nas:~ # cat ~/.config/rclone/rclone.conf
# Encrypted rclone configuration File

RCLONE_ENCRYPT_V0:
g3M***LSA=

А при роботі з rclone він буде просити ввести пароль для читання конфігу:

root@setevoy-nas:~ # rclone config show
Enter configuration password:

Для автоматизації в скриптах або cron пароль можна передати через змінну RCLONE_CONFIG_PASS.

Див. rclone config encryption та rclone config show.

Основні можливості і команди

Головні команди, принаймні при використанні rclone для бекапів, це rclone copy та rclone sync.

З найбільш цікавих команд варто глянути такі:

  • rclone bisync: повністю двосторонньо синхронізує src та dst (копіює і видаляє)
  • rclone cat та rclone rcat: читання з remote у stdout або запис у remote зі stdout
  • rclone delete та rclone deletefile: видалення даних з можливістю використання фільтрів (--exclude/--exclude)
  • rclone ls та rclone lsd: ls – список файлів з розмірами, lsd – список директорій
  • rclone mkdir: створення директорії на remote
  • rclone mount: монтує remote як файлову систему (FUSE)
  • rclone move: переносить файли (copy + delete з src)
  • rclone ncdu: інтерактивний перегляд використання місця на remote
  • rclone rmdir: видалення порожньої директорії
  • rclone rmdirs: рекурсивне видалення порожніх директорій
  • rclone purge: повне видалення каталогу з усім вмістом
  • rclone size: показує кількість файлів і загальний розмір
  • rclone test speed: тест швидкості upload/download до remote
  • rclone tree: показує дерево каталогів

Корисні Flags

Див. всі в Global Flags.

Найбільш цікаві:

  • --check-first: виконати перевірку даних між src та dst до початку копіювання
  • --checksum: порівнювати файл між src та dst не за size+mtime, а по MD5SUM checksum – повільніше, але точніше, корисно для критичних даних
  • --immutable: не змінювати файл в dst, якщо він відрізняється від src, а впасти з помилкою
  • --interactive: підтвердження змін вручну
  • --dry-run: тестове виконання без копіювання
  • --progress: відобразити процес
  • --transfers N: кількість файлів, які копіюються одночасно (дефолт 4)
  • --create-empty-src-dirs: якщо src каталог порожній – то створити порожній каталог в dst (не працює з S3)
  • --exclude та --exclude-from, --include та --include-from: список або файл зі списком даних, які треба включити або виключити з копіювання, див. Filter
  • --log-file: куди писати лог (корисно для автоматизації)
  • --fast-list: створює один великий список директорій і файлів, який тримає в пам’яті, а не для кожної директорії окремо (більше пам’яті – але швидше і менше API-запитів до dst)
  • --update: пропустити файли, modification time яких на dst новіший, ніж в src
  • --human-readable: використовувати формат Ki/Mi/Gi

Використання rclone copy та rclone copyto

rclone copy просто копіює файл або директорію до заданого remote.

Якщо src – каталог з підкаталогами, то будуть скопійовані всі дані і збережено структуру каталогів.

Наприклад, маємо локально каталоги з файлами:

root@setevoy-nas:~ # tree /tmp/new/
/tmp/new/
└── another
    └── dir
        ├── a.txt
        └── sub
            └── b.txt

Запускаємо rclone copy:

root@setevoy-nas:~ # rclone copy /tmp/new/ nas-google-drive:Backups/Rclone

Перевіряємо на remote:

root@setevoy-nas:~ # rclone tree nas-google-drive:Backups/Rclone/
/
└── another
    └── dir
        ├── a.txt
        └── sub
            └── b.txt

Аналогічно, можемо просто скопіювати окремий файл в новий каталог:

root@setevoy-nas:~ # rclone copy /root/rclone-copy.txt nas-google-drive:Backups/Rclone/rclone-dir

І тепер:

root@setevoy-nas:~ # rclone ls nas-google-drive:Backups/Rclone
        5 rclone-dir/rclone-copy.txt
        2 another/dir/a.txt
        2 another/dir/sub/b.txt

Але з rclone copy не можна вказати нове ім’я файлу, тобто:

root@setevoy-nas:~ # rclone copy /root/rclone-copy.txt nas-google-drive:Backups/Rclone/rclone-dir/new-rclone-copy.txt

Створить нову директорію, а не файл з ім’ям new-rclone-copy.txt:

root@setevoy-nas:~ # rclone ls nas-google-drive:Backups/Rclone
        5 rclone-dir/rclone-copy.txt
        5 rclone-dir/new-rclone-copy.txt/rclone-copy.txt

Для копіювання з новим іменем використовуємо rclone copyto:

root@setevoy-nas:~ # rclone copyto /root/rclone-copy.txt nas-google-drive:Backups/Rclone/rclone-dir-copyto/new-rclone-copyto.txt

В результаті:

root@setevoy-nas:~ # rclone ls nas-google-drive:Backups/Rclone
        5 rclone-dir-copyto/new-rclone-copyto.txt
...

Використання rclone sync

rclone sync виконує повну синхронізацію між src та dst: якщо в src файл було видалено – він видалиться і на dst. Див. також rclone bisync.

Корисні флаги тут:

  • --backup-dir: на dst не видаляти файл, який змінився в src, а зберегти в окрему директорію
  • --delete-after та --delete-before: видаляти дані до або після успішного копіювання
  • --suffix: додати суфікс до даних, які змінились

З rclone purge видаляємо створені під час тестів вище дані (видалить і саму директорію):

root@setevoy-nas:~ # rclone purge nas-google-drive:Backups/Rclone/
root@setevoy-nas:~ # rclone mkdir nas-google-drive:Backups/Rclone/

Виконуємо rclone sync:

root@setevoy-nas:~ # rclone sync /tmp/new/ nas-google-drive:Backups/Rclone/

Отримуємо аналогічну структуру на remote:

root@setevoy-nas:~ # rclone tree nas-google-drive:Backups/Rclone/
/
└── another
    └── dir
        ├── a.txt
        └── sub
            └── b.txt

І приклад того, як працює --backup-dir.

Змінимо зміст файлу в src:

root@setevoy-nas:~ # echo updated > /tmp/new/another/dir/a.txt

Виконуємо rclone sync і вказуємо --backup-dir:

root@setevoy-nas:~ # rclone sync /tmp/new/ nas-google-drive:Backups/Rclone --backup-dir nas-google-drive:Backups/Rclone-changed/$(date +%Y-%m-%d-%H-%M-%S)

Але --backup-dir має бути поза dst – тобто не можна робити rclone sync /path/src/ nas-google-drive:path/dst/ --backup-dir path/dst/backupDir.

Тепер у нас в корні Backups/ є новий каталог Rclone-changed/:

root@setevoy-nas:~ # rclone tree nas-google-drive:Backups/
/
├── Rclone
│   └── another
│       └── dir
│           ├── a.txt
│           └── sub
│               └── b.txt
└── Rclone-changed
    └── 2026-01-21-12-08-33
        └── another
            └── dir
                └── a.txt

В якому збережено оригінальну копію файлу a.txt:

root@setevoy-nas:~ # rclone cat nas-google-drive:Backups/Rclone-changed/2026-01-21-12-08-33/another/dir/a.txt
a

А в Backups/Rclone/another/dir/a.txt у нас оновлений файл:

root@setevoy-nas:~ # rclone cat nas-google-drive:Backups/Rclone/another/dir/a.txt
updated

Ну і на цьому, мабуть, все.

Тепер можна створити кілька cron і налаштувати бекап бекапів.

Loading

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

19 Січня 2026

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# Generated by NetworkManager
nameserver 192.168.0.1

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

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

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

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

Може, dnsmasq?

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

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

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

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

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

IPv6 та DNS

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

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

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

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

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

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

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

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

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

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

NIC Offloading

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

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

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

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

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

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

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

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

NIC Power Management

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

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

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

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

$ sudo ethtool --set-eee enp0s31f6 eee off

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

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

$ ping -i 0.2 192.168.0.1

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

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

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

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

Або:

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

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

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

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

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

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

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

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

Параметри interrupts:

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

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

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

options e1000e IntMode=0

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

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

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

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

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

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

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

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

Або з ethtool:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading

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

5 Січня 2026

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

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

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

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

Всі пости цієї серії:

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

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

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

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

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

FreeBSD backup plan

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

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

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

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

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

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

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

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

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

Restic overview

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

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

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

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

$ sudo pacman -S restic

Та на FreeBSD:

# pkg install restic

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

Restic repositories та snapshots

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Тут:

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

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

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

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

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

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

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

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

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

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

$ pwgen 32 1
xoo8eibia2ohch7Oat7zeeshahn0keic

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

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

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

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

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

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

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

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

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

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

Restic backup та restore

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Include та exclude для backup та restore

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

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

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

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

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

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

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

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

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

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

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

# MAIN
/home/setevoy/Photos
...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Наприклад:

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

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

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

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

Наприклад:

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

Тут:

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

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

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

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

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

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

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

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

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

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

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

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

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

$ restic init -r new-test-repo

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Restic та Google Drive через rclone

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading

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

2 Січня 2026

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

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

Investigating the issue

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

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

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

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

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

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

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

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

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

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

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

Шта?…

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

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

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

Тобто:

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

Окей…

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

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

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

А там…

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

Mitigating the issue

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

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

Linux Kernel TCP tuning

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

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

Тут:

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

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

 

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

Iptables та DROP by Source Address

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

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

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

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

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

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

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

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

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

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

Iptables та DROP by Destination Address

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

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

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

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

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

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

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

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

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

# apt install iptables-persistent

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

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

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

Готово.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

HackerNews, ycombinator? Вау…

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

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

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

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

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

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

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

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

Loading

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

31 Грудня 2025

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

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

Всі пости цієї серії:

NFSv3 vs NFSv4

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

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

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

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

Створення ZFS datasets

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

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

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

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

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

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

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

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

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

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

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

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

NFS, ZFS та sharenfs 

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

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

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

V4: /nas/nfs

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

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

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

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

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

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

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

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

/nas/nfs/backups        -network 192.168.0.0/24

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NFS, users та “Permission denied”

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ZFS sharenfs та muliple -network

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

root@setevoy-nas:~ #

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

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

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

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

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

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

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

З адресою 10.8.0.3:

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

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

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

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

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

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

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

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

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

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

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

Linux, /etc/fstab та systemd-automount

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

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

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

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

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

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

...

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

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

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

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

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

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

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

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

Готово.

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

Loading