26 Commits

Author SHA1 Message Date
bbaf0304c3 Merge pull request 'v0.3.0' (#3) from develop into main
Reviewed-on: #3
2026-01-04 17:09:39 +05:00
1f8be77ab3 Clarify Docker support status in English README 2026-01-04 16:39:21 +05:00
d2795639da Update Russian README: reorder sections and clarify Docker support status 2026-01-04 16:39:08 +05:00
8638c49886 Add "Requirements" section to English README 2026-01-04 16:37:16 +05:00
66e6bad111 Add system requirements section to README 2026-01-04 16:37:06 +05:00
1a6d6b813b Update CHANGELOG.md with release date for version 0.3.0 2026-01-04 16:36:36 +05:00
9b8d07ccb3 Fix typo in CHANGELOG.md: correct WantedBy target from sysinit.target to multi-user.target 2026-01-04 16:20:05 +05:00
4b8622a870 Update CHANGELOG.md with partial Docker support details for version 0.3.0 2026-01-04 16:19:30 +05:00
b9719f7eaf Add Docker event monitoring and chain clearing functionality
- Introduced `Events` method in Docker client to stream and handle Docker events.
- Added `Clear` method to nftables chain interface for clearing rules.
- Enhanced daemon lifecycle to include Docker event monitoring when Docker support is enabled.
- Updated nftables rule management with event-driven chain clearing and reloading.
2026-01-04 16:06:01 +05:00
c424621615 Add Docker support with nftables integration
- Introduced Docker monitoring to manage nftables rules.
- Added `docker_support` option to firewall configuration.
- Integrated Docker bridge, container handling, and related network rules.
- Updated default configurations for Docker path and settings.
- Enhanced `daemon` lifecycle for Docker integration.
2026-01-04 13:59:26 +05:00
865f12d966 Update dependencies: bump go-nftables-client to v0.1.1 and make go-mail a direct dependency 2026-01-01 22:06:50 +05:00
b3a94855b8 Refactor localOutput receiver names for consistency in AddRule and AddRuleOut methods 2026-01-01 20:28:54 +05:00
4d001a026c Refactor localInput receiver names for consistency in AddRule and AddRuleIn methods 2026-01-01 20:28:37 +05:00
6e4bd17bfe Update CHANGELOG.md to include new configuration files notifications.toml and analyzer.toml 2025-12-31 23:14:09 +05:00
0bcdb7bcc7 Update LICENSE-3RD-PARTY.txt to include go-mail dependency and its MIT license details 2025-12-31 23:05:56 +05:00
5f2d5a1a9e Simplify EmptyAnalysis.Process by ignoring unused parameter 2025-12-31 23:01:20 +05:00
542f7415b7 Update CHANGELOG.md with email notification and SSH login notification details for version 0.3.0 2025-12-31 22:58:25 +05:00
8615c79f12 Refactor log analyzer to support SSH login detection
- Moved `Entry` type to `analysis` package for better organization.
- Introduced `SSH` analysis service to detect and notify about SSH logins.
- Added notification and logging for detected SSH login events.
2025-12-31 22:52:12 +05:00
b5686a2ee6 Add systemd log integration for analyzer service
- Implemented `systemd` log monitoring using `journalctl`.
- Added `BinPath` configuration for specifying binary paths.
- Introduced `ssh` unit monitoring for authorization tracking.
- Updated analyzer lifecycle to integrate log processing.
- Enhanced validation for `journalctl` path in settings.
- Updated default configurations with `journalctl` path.
2025-12-30 20:57:35 +05:00
e78685c130 Add support for analyzer service and configuration
- Introduced `analyzer` service for log parsing and authorization tracking.
- Added dedicated analyzer configuration via `analyzer.toml`.
- Integrated analyzer setup and lifecycle management into daemon runtime.
- Enhanced `setting` package to include analyzer settings parsing and validation.
- Updated daemon options to support analyzer configuration.
- Extended default configuration files for analyzer settings.
2025-12-30 15:03:41 +05:00
74dce294bf Add support for email notifications
- Introduced email notifications enabling configuration via `notifications.toml`.
- Created notification handling within `internal/daemon/notifications`.
- Added async email queue with error handling and customizable TLS configurations.
- Integrated notifications setup and validation into the daemon runtime.
2025-12-16 19:30:18 +05:00
6929ac9bf5 Update systemd service file for kor-elf-shield to improve reliability
- Added `Restart=on-failure` with a 10-second delay.
- Changed `WantedBy` target to `multi-user.target`.
- Defined service type as `simple`.
2025-12-08 23:19:38 +05:00
69157c90cb Merge pull request 'v0.2.0' (#2) from develop into main
Reviewed-on: #2
2025-11-29 16:12:03 +05:00
7054efd359 Update CHANGELOG.md with release date for version 0.2.0 2025-11-29 15:41:12 +05:00
57948fb639 Add support for chain priority configuration in nftables
- Introduced `input_priority`, `output_priority`, and `forward_priority` options in `firewall.toml`.
- Updated `chains` and chain creation functions to include priority handling.
- Added validation for priority values to ensure they remain within the acceptable range (-50 to 50).
- Adjusted `reloadInput`, `reloadOutput`, and `reloadForward` to respect priority settings.
2025-11-29 15:38:58 +05:00
6e7b6093f1 Add support for clear_mode option to toggle nftables clearing behavior
- Introduced `clear_mode` parameter in `firewall.toml` with options for clearing all nftables rules (`global`) or table-specific rules (`own`).
- Updated `chains` and `firewall` logic to respect `clear_mode` configuration.
- Enhanced `options` parsing and validation for `clear_mode`.
- Updated `CHANGELOG.md` to reflect the addition of `clear_mode`.
2025-11-25 20:58:12 +05:00
61 changed files with 2583 additions and 64 deletions

View File

@@ -1,3 +1,44 @@
## 0.3.0 (4.1.2026)
***
#### Русский
* Добавлена частичная поддержка Docker.
* Добавлен параметр options.docker_support в firewall.toml. Это включает поддержку Docker.
* Каждый запуск контейнера будет полностью пересчитываться правила у chain, которые относятся к Docker. (в будущем планирую это переработать)
* Добавлены настройки для уведомлений по электронной почте.
* Добавлен файл настроек notifications.toml.
* Реализовано уведомление о входах по SSH.
* Добавлен файл настроек analyzer.toml.
* Служба systemd
* Изменено WantedBy с sysinit.target на multi-user.target
* Убрано ExecStop. По факту это не работало. Чтобы остановить сервис с очисткой правил nftables выпоните команду: kor-elf-shield stop
* Добавлено Restart=on-failure. Нужно для того, чтобы программа перезапустилась после критической ошибки.
***
#### English
* Added partial Docker support.
* Added the options.docker_support parameter to firewall.toml. This enables Docker support.
* Each container launch will completely recalculate the Docker-specific rules in chain. (I plan to rework this in the future)
* Added settings for email notifications.
* Added notifications.toml settings file.
* Implemented notification of SSH logins.
* Added analyzer.toml settings file.
* Systemd service
* Changed WantedBy from sysinit.target to multi-user.target
* Removed ExecStop. It didn't actually work. To stop the service and clear the nftables rules, run the command: kor-elf-shield stop
* Added Restart=on-failure. This is necessary to ensure the program restarts after a critical error.
## 0.2.0 (29.11.2025)
***
#### Русский
* Добавлен параметр clear_mode в firewall.toml. Он позволяет переключать режим очистки всех правил в nftables или только таблицу относящие к программе.
* Добавлен параметр input_priority в firewall.toml. Можно указать приоритет от -50 по 50 к chain input.
* Добавлен параметр output_priority в firewall.toml. Можно указать приоритет от -50 по 50 к chain output.
* Добавлен параметр forward_priority в firewall.toml. Можно указать приоритет от -50 по 50 к chain forward.
***
#### English
* Added the clear_mode parameter to firewall.toml. It allows you to toggle clearing of all rules in nftables or only the program-specific table.
* Added the input_priority parameter to firewall.toml. You can specify a priority from -50 to 50 for chain input.
* Added the output_priority parameter to firewall.toml. You can specify a priority from -50 to 50 for chain output.
* Added the forward_priority parameter to firewall.toml. You can specify a priority from -50 to 50 for chain forward.
***
## 0.1.0 (8.11.2025)
***
#### Русский

View File

@@ -684,6 +684,32 @@ SOFTWARE.
--------------------------------------------------------------------------------
github.com/wneessen/go-mail
MIT License
Copyright (c) 2022-2025 The go-mail Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
go.uber.org/multierr
Copyright (c) 2017-2021 Uber Technologies, Inc.

View File

@@ -6,6 +6,15 @@
***
<p style="color: red; font-weight: bold; font-size: 20px;">Требования:</p>
* Запуск от имени root
* Linux 5.2+
* nftables
* Systemd
***
### Сделанно:
* Реализована возможность настраивать nftables:
* По умолчанию разрешить или блокировать входящий трафик.
@@ -14,11 +23,11 @@
* Настройка портов.
* Настройка белых и чёрных списков IP адресов.
* Настройка логирование.
### В планах:
* Подружить с docker.
* Подружить с docker (частично).
* Внедрить настройку уведомлений (пока только e-mail).
* Отправлять уведомления при авторизации ssh.
### В планах:
* Защита от перебора паролей (brute-force).
* Уведомлять, если появится новый пользователь в системе.
* Уведомлять, если изменились системные файлы.

View File

@@ -6,6 +6,15 @@
***
<p style="color: red; font-weight: bold; font-size: 20px;">Requirements:</p>
* Run as root
* Linux 5.2+
* nftables
* Systemd
***
### Done:
* The ability to configure nftables has been implemented:
* Allow or block incoming traffic by default.
@@ -14,11 +23,11 @@
* Port configuration.
* Setting up white and black lists of IP addresses.
* Setting up logging.
### The plans include:
* Make friends with docker.
* Make friends with docker (partially).
* Implement notification settings (for now only by e-mail).
* Send notifications during ssh authorization.
### The plans include:
* Password brute-force protection.
* Notify if a new user appears in the system.
* Notify if system files have changed.

View File

@@ -0,0 +1,47 @@
###############################################################################
# РАЗДЕЛ:Отслеживать авторизаций
# ***
# SECTION:Track authorizations
###############################################################################
[login]
###
# Включает группу отслеживания авторизации.
# Если отключено, отслеживание авторизации работать не будет.
# По умолчанию: true
# ***
# Enables the authorization tracking group.
# If disabled, no authorization tracking will work.
# Default: true
###
enabled = true
###
# Включает уведомления об авторизации.
# Если отключено, они будут отображаться в логах только на уровне = "info".
# По умолчанию: true
# ***
# Enables authorization notifications.
# If disabled, they will only appear in the logs under level = "info".
# Default: true
###
notify = true
###
# Включает отслеживание авторизации по ssh.
# По умолчанию: true
# ***
# Enables tracking of SSH authorization.
# Default: true
###
ssh_enable = true
###
# Включает уведомления об авторизации по ssh.
# Если отключено, они будут отображаться в логах только на уровне = "info".
# По умолчанию: true
# ***
# Enables SSH authorization notifications.
# If disabled, they will only appear in the logs under level = "info".
# Default: true
###
ssh_notify = true

View File

@@ -299,6 +299,26 @@ icmp_strict = false
# SECTION:General Settings
###############################################################################
[options]
###
# Переключения режима очистки фаервола nftables. Если указать "own", то может получиться конфликт в правилах.
# Может спровоцировать проблему в безопасности. Указывайте "own" если вы уверены в своих действиях.
# Допустимые значения:
# global = очищает полностью все правила
# own = очищает только правила от таблицы, которые указаны в параметре table_name
#
# По умолчанию: global
# ***
# Switching the nftables firewall cleaning mode. If you specify "own", a conflict in the rules may occur.
# This may cause a security issue. Use "own" if you are confident in your actions.
# Valid values:
# global = clears all rules completely
# own = clears only the rules from the table that are specified in the table_name parameter
#
# Default: global
###
clear_mode = "global"
###
# Будет ли демон сохранять правила в системный файл nftables.
# Не забудьте проверить, что путь к nftables соответствует вашей ОС.
@@ -319,6 +339,15 @@ saves_rules = false
###
saves_rules_path = "/etc/nftables.conf"
###
# Включает поддержку docker.
# По умолчанию: false
# ***
# Includes docker support.
# Default: false
###
docker_support = false
###
# Включает строгие правила nftables к DNS-трафику. Если включить этот режим, то некоторые правила,
# связанные с DNS, не добавятся в nftables. Что улучшит безопасность и предотвратить злоупотребление
@@ -409,6 +438,21 @@ default_allow_forward = false
###
input_drop = "drop"
###
# Приоритет chain для input.
# От: -50
# По: 50
#
# По умолчанию: -10
# ***
# Chain priority for input.
# From: -50
# To: 50
#
# Default: -10
###
input_priority = -10
###
# Как заблокировать исходящий трафик. Блокировать молча или с обратной связью.
# Допустимые значения:
@@ -426,6 +470,21 @@ input_drop = "drop"
###
output_drop = "reject"
###
# Приоритет chain для output.
# От: -50
# По: 50
#
# По умолчанию: -10
# ***
# Chain priority for output.
# From: -50
# To: 50
#
# Default: -10
###
output_priority = -10
###
# Как заблокировать трафик forward. Блокировать молча или с обратной связью.
# Допустимые значения:
@@ -443,6 +502,21 @@ output_drop = "reject"
###
forward_drop = "drop"
###
# Приоритет chain для forward.
# От: -50
# По: 50
#
# По умолчанию: -10
# ***
# Chain priority for forward.
# From: -50
# To: 50
#
# Default: -10
###
forward_priority = -10
###############################################################################
# РАЗДЕЛ:Именование метаданных
# ***
@@ -483,4 +557,4 @@ chain_output_name = "output"
# Chain name for forward
# Default: "forward"
###
chain_forward_name = "forward"
chain_forward_name = "forward"

View File

@@ -192,6 +192,24 @@ log_error_paths = ["stderr"]
###
nftables = "/usr/sbin/nft"
###
# Укажите путь к journalctl. Возможно в вашей ОС путь может отличаться.
# По умолчанию: /bin/journalctl
# ***
# Specify the path to journalctl. The path may differ in your OS.
# Default: /bin/journalctl
###
journalctl = "/bin/journalctl"
###
# Укажите путь к docker. Возможно в вашей ОС путь может отличаться.
# По умолчанию: /usr/bin/docker
# ***
# Specify the path to docker. The path may differ in your OS.
# Default: /usr/bin/docker
###
docker = "/usr/bin/docker"
###############################################################################
# РАЗДЕЛ:Пути к другим настройкам
# ***
@@ -210,3 +228,36 @@ nftables = "/usr/sbin/nft"
# Default: /etc/kor-elf-shield/firewall.toml
###
firewall = "/etc/kor-elf-shield/firewall.toml"
###
# Укажите путь к настройкам уведомлений.
# Файл должен иметь расширение .toml.
# По умолчанию: /etc/kor-elf-shield/notifications.toml
# ***
# Specify the path to notification settings.
# The file must have the .toml extension.
# Default: /etc/kor-elf-shield/notifications.toml
###
notifications = "/etc/kor-elf-shield/notifications.toml"
###
# Укажите путь к настройкам парсинга логов.
# Файл должен иметь расширение .toml.
# По умолчанию: /etc/kor-elf-shield/analyzer.toml
# ***
# Specify the path to the log parsing settings.
# The file must have the .toml extension.
# Default: /etc/kor-elf-shield/analyzer.toml
###
analyzer = "/etc/kor-elf-shield/analyzer.toml"
###
# Укажите путь к настройкам docker.
# Файл должен иметь расширение .toml.
# По умолчанию: /etc/kor-elf-shield/docker.toml
# ***
# Specify the path to the docker settings.
# The file must have the .toml extension.
# Default: /etc/kor-elf-shield/docker.toml
###
docker = "/etc/kor-elf-shield/docker.toml"

View File

@@ -0,0 +1,170 @@
###############################################################################
# РАЗДЕЛ:Базовые настройки
# ***
# SECTION:Basic settings
###############################################################################
###
# Включает или выключает уведомления.
# !!! Не забудьте перед включением настроить email !!!
# false = Выключает.
# true = Включает.
#
# По умолчанию: false
# ***
# Turns notifications on or off.
# !!! Don't forget to set up your email before turning it on !!!
# false = Disables.
# true = Enables.
#
# Default: false
###
enabled = false
###
# Название сервера в уведомлениях
# По умолчанию: server
# ***
# Server name in notifications
# Default: server
###
server_name = "server"
###############################################################################
# РАЗДЕЛ:email
# ***
# SECTION:email
###############################################################################
[email]
###
# Сервер, через который будет отправляться почта.
# Например: smtp.gmail.com
# По умолчанию:
# ***
# The server through which mail will be sent.
# For example: smtp.gmail.com
# Default:
###
host = ""
###
# Указать порт сервера, через который будет отправляться почта.
# Например: 587
# По умолчанию:
# ***
# Specify the server port through which mail will be sent.
# For example: 587
# Default:
###
port = ""
###
# Логин к серверу, через который будет отправляться почта.
# По умолчанию:
# ***
# Login to the server through which mail will be sent.
# Default:
###
username = ""
###
# Пароль к серверу, через который будет отправляться почта.
# По умолчанию:
# ***
# Password for the server through which mail will be sent.
# Default:
###
password = ""
###
# Тип авторизации.
# Варианты: "PLAIN", "LOGIN", "CRAM-MD5", "NONE"
# Обычно используется "PLAIN". Если у вас внутренний релей без пароля - используйте "NONE".
# По умолчанию: "PLAIN"
# ***
# Authorization type.
# Options: "PLAIN", "LOGIN", "CRAM-MD5", "NONE"
# Usually "PLAIN" is used. If you have an internal relay without a password - use "NONE".
# Default: "PLAIN"
###
auth_type = "PLAIN"
###
# Защищённое соединение.
# Варианты: "NONE", "STARTTLS", "IMPLICIT"
#
# "NONE" — без TLS
# "STARTTLS" — обычный SMTP на 587 (или 25) + upgrade через STARTTLS
# "IMPLICIT" — SMTPS (TLS сразу), обычно 465
#
# По умолчанию: "STARTTLS"
# ***
# Secure connection.
# Options: "NONE", "STARTTLS", "IMPLICIT"
#
# "NONE" — without TLS
# "STARTTLS" — regular SMTP on 587 (or 25) + upgrade via STARTTLS
# "IMPLICIT" — SMTPS (TLS Immediately), typically 465
#
# Default: "STARTTLS"
###
tls_mode = "STARTTLS"
###
# Только если тип защищённого соединения в режиме starttls.
# Варианты: "MANDATORY", "OPPORTUNISTIC"
#
# "MANDATORY" — если STARTTLS недоступен/не удался будет вызвана ошибка
# "OPPORTUNISTIC" — попытаться STARTTLS, но если нельзя, то попытается отправить без TLS
#
# По умолчанию: "MANDATORY"
# ***
# Only if the secure connection type is in starttls mode.
# Options: "MANDATORY", "OPPORTUNISTIC"
#
# "MANDATORY" — if STARTTLS is unavailable/failed, an error will be raised
# "OPPORTUNISTIC" — try STARTTLS, but if that fails, it will try to send without TLS
#
# Default: "MANDATORY"
###
tls_policy = "MANDATORY"
###
# Проверять ли сертификат защищённого соединения.
#
# false = Выключает.
# true = Включает.
#
# По умолчанию: true
# ***
# Whether to check the secure connection certificate.
#
# false = Disables.
# true = Enables.
#
# Default: true
###
tls_verify = true
###
# Email, который будет указываться при отправки почты.
# Например: test@localhost
# По умолчанию:
# ***
# Email that will be specified when sending mail.
# For example: test@localhost
# Default:
###
from = ""
###
# Адрес электронной почты, на который будет отправлено письмо.
# Например: root@localhost
# По умолчанию:
# ***
# Email to whom the mail will be sent.
# For example: root@localhost
# Default:
###
to = ""

View File

@@ -3,8 +3,10 @@ Description=kor-elf-shield
After=network.target
[Service]
Type=simple
ExecStart=/usr/sbin/kor-elf-shield start
ExecStop=/usr/sbin/kor-elf-shield stop
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=sysinit.target
WantedBy=multi-user.target

3
go.mod
View File

@@ -3,16 +3,17 @@ module git.kor-elf.net/kor-elf-shield/kor-elf-shield
go 1.25
require (
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1
github.com/nicksnyder/go-i18n/v2 v2.6.0
github.com/spf13/viper v1.21.0
github.com/urfave/cli/v3 v3.4.1
github.com/wneessen/go-mail v0.7.2
go.uber.org/zap v1.27.0
golang.org/x/sys v0.36.0
golang.org/x/text v0.29.0
)
require (
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect

6
go.sum
View File

@@ -1,5 +1,5 @@
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.0 h1:jglai6XEk1uSCxd1TEpx6IBqWhkc+KgonV6rUDTkyyU=
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.0/go.mod h1:a7F+XdL1pK5P3ucQRR2EK/fABAP37LLBENiA4hX7L6A=
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1 h1:3oGtZ/r1YAdlvI16OkZSCaxcWztHe/33ITWfI2LaQm0=
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1/go.mod h1:a7F+XdL1pK5P3ucQRR2EK/fABAP37LLBENiA4hX7L6A=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -40,6 +40,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=

View File

@@ -2,8 +2,11 @@ package daemon
import (
"context"
"fmt"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
@@ -43,7 +46,21 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
return err
}
d, err := daemon.NewDaemon(config, logger)
notificationsService, err := newNotificationsService(logger)
if err != nil {
logger.Fatal(err.Error())
// Fatal should call os.Exit(1), but there's a chance that might not happen,
// so we return err just in case.return err
return err
}
dockerService, err := newDockerService(ctx, logger, config.ConfigFirewall.Options.DockerSupport)
if err != nil {
logger.Error(fmt.Sprintf("Failed to create docker service: %s", err))
}
d, err := daemon.NewDaemon(config, logger, notificationsService, dockerService)
if err != nil {
logger.Fatal(err.Error())
@@ -63,3 +80,27 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
return nil
}
func newNotificationsService(logger log.Logger) (notifications.Notifications, error) {
config, err := setting.Config.OtherSettingsPath.ToNotificationsConfig()
if err != nil {
return nil, err
}
return notifications.New(config, logger), nil
}
func newDockerService(ctx context.Context, logger log.Logger, dockerSupport bool) (dockerService docker_monitor.Docker, err error) {
if dockerSupport {
dockerPath := setting.Config.BinaryLocations.Docker
if dockerPath == "" {
return docker_monitor.NewDockerNotSupport(), fmt.Errorf("docker path is empty")
}
dockerService = docker_monitor.New(dockerPath, ctx, logger)
} else {
dockerService = docker_monitor.NewDockerNotSupport()
}
return dockerService, nil
}

View File

@@ -0,0 +1,81 @@
package analyzer
import (
"context"
"fmt"
config2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
analyzerLog "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log"
analysisServices "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Analyzer interface {
Run(ctx context.Context)
Close() error
}
type analyzer struct {
config config2.Config
logger log.Logger
notify notifications.Notifications
systemd analyzerLog.Systemd
analysis analyzerLog.Analysis
}
func New(config config2.Config, logger log.Logger, notify notifications.Notifications) Analyzer {
units := []string{}
if config.Login.Enabled && config.Login.SSH.Enabled {
units = append(units, "ssh")
}
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, units, logger)
analysisService := analyzerLog.NewAnalysis(&config, logger, notify)
return &analyzer{
config: config,
logger: logger,
notify: notify,
systemd: systemdService,
analysis: analysisService,
}
}
func (a *analyzer) Run(ctx context.Context) {
logChan := make(chan analysisServices.Entry, 1000)
go a.systemd.Run(ctx, logChan)
go a.processLogs(ctx, logChan)
a.logger.Debug("Analyzer is start")
}
func (a *analyzer) processLogs(ctx context.Context, logChan <-chan analysisServices.Entry) {
for {
select {
case <-ctx.Done():
return
case entry := <-logChan:
a.logger.Debug(fmt.Sprintf("Received log entry: %s", entry))
switch entry.Unit {
case "ssh.service":
if err := a.analysis.SSH(&entry); err != nil {
a.logger.Error(fmt.Sprintf("Failed to analyze SSH logs: %s", err))
}
break
default:
a.logger.Warn(fmt.Sprintf("Unknown unit: %s", entry.Unit))
}
}
}
}
func (a *analyzer) Close() error {
if err := a.systemd.Close(); err != nil {
return err
}
a.logger.Debug("Analyzer is stop")
return nil
}

View File

@@ -0,0 +1,5 @@
package config
type BinPath struct {
Journalctl string
}

View File

@@ -0,0 +1,6 @@
package config
type Config struct {
BinPath BinPath
Login Login
}

View File

@@ -0,0 +1,12 @@
package config
type Login struct {
Enabled bool
Notify bool
SSH LoginSSH
}
type LoginSSH struct {
Enabled bool
Notify bool
}

View File

@@ -0,0 +1,31 @@
package log
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
analysisServices "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Analysis interface {
SSH(entry *analysisServices.Entry) error
}
type analysis struct {
sshService analysisServices.Analysis
logger log.Logger
notify notifications.Notifications
}
func NewAnalysis(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
return &analysis{
sshService: analysisServices.NewSSH(config, logger, notify),
logger: logger,
notify: notify,
}
}
func (a *analysis) SSH(entry *analysisServices.Entry) error {
return a.sshService.Process(entry)
}

View File

@@ -0,0 +1,22 @@
package analysis
import (
"time"
)
type Analysis interface {
Process(entry *Entry) error
}
type Entry struct {
Message string
Unit string
PID string
Time time.Time
}
type EmptyAnalysis struct{}
func (empty *EmptyAnalysis) Process(_ *Entry) error {
return nil
}

View File

@@ -0,0 +1,87 @@
package analysis
import (
"fmt"
"regexp"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type ssh struct {
login sshLogin
logger log.Logger
notify notifications.Notifications
}
type sshLogin struct {
enabled bool
notify bool
}
type sshProcessReturn struct {
found bool
subject string
body string
}
func NewSSH(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
if !config.Login.Enabled || !config.Login.SSH.Enabled {
return &EmptyAnalysis{}
}
return &ssh{
login: sshLogin{
enabled: config.Login.Enabled && config.Login.SSH.Enabled,
notify: config.Login.Notify && config.Login.SSH.Notify,
},
logger: logger,
notify: notify,
}
}
func (s *ssh) Process(entry *Entry) error {
if s.login.enabled {
result, err := s.login.process(entry)
if err != nil {
s.logger.Error(fmt.Sprintf("Failed to process ssh login: %s", err))
} else if result.found {
if s.login.notify {
s.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
}
s.logger.Info(fmt.Sprintf("SSH login detected: %s", entry.Message))
}
}
return nil
}
func (l *sshLogin) process(entry *Entry) (sshProcessReturn, error) {
re := regexp.MustCompile(`^Accepted (\S+) for (\S+) from (\S+) port \S+`)
matches := re.FindStringSubmatch(entry.Message)
if matches != nil {
user := matches[2]
ip := matches[3]
return sshProcessReturn{
found: true,
subject: i18n.Lang.T("alert.login.subject", map[string]any{
"User": user,
"IP": ip,
}),
body: i18n.Lang.T("alert.login.body", map[string]any{
"User": user,
"IP": ip,
"Log": entry.Message,
"Time": entry.Time,
}),
}, nil
}
return sshProcessReturn{found: false}, nil
}

View File

@@ -0,0 +1,147 @@
package log
import (
"context"
"encoding/json"
"fmt"
"io"
"os/exec"
"strconv"
"sync"
"time"
analysisServices "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Systemd interface {
Run(ctx context.Context, logChan chan<- analysisServices.Entry)
Close() error
}
type systemd struct {
path string
units []string
logger log.Logger
cmd *exec.Cmd
mu sync.Mutex
}
type journalRawEntry struct {
Message string `json:"MESSAGE"`
Unit string `json:"_SYSTEMD_UNIT"`
PID string `json:"_PID"`
SourceTimestamp string `json:"_SOURCE_REALTIME_TIMESTAMP"`
RealtimeTimestamp string `json:"__REALTIME_TIMESTAMP"`
}
func NewSystemd(path string, units []string, logger log.Logger) Systemd {
return &systemd{
path: path,
units: units,
logger: logger,
}
}
func (s *systemd) Run(ctx context.Context, logChan chan<- analysisServices.Entry) {
if len(s.units) == 0 {
s.logger.Debug("No units specified for journalctl")
return
}
args := []string{"-f", "-n", "0", "-o", "json"}
for _, unit := range s.units {
args = append(args, "-u", unit)
}
s.logger.Debug("Journalctl started")
for {
select {
case <-ctx.Done():
return
default:
if err := s.watch(ctx, args, logChan); err != nil {
s.logger.Error(fmt.Sprintf("Journalctl exited with error: %v", err))
}
// Pause before restarting to avoid CPU load during persistent errors
select {
case <-ctx.Done():
return
case <-time.After(5 * time.Second):
s.logger.Warn("Journalctl connection lost. Restarting in 5s...")
continue
}
}
}
}
func (s *systemd) watch(ctx context.Context, args []string, logChan chan<- analysisServices.Entry) error {
cmd := exec.CommandContext(ctx, s.path, args...)
s.mu.Lock()
s.cmd = cmd
s.mu.Unlock()
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe error: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start error: %w", err)
}
decoder := json.NewDecoder(stdout)
for {
var raw journalRawEntry
if err := decoder.Decode(&raw); err != nil {
if err == io.EOF {
break // The process terminated normally or was killed.
}
return fmt.Errorf("decode error: %w", err)
}
tsStr := raw.SourceTimestamp
if tsStr == "" {
tsStr = raw.RealtimeTimestamp
}
var entryTime time.Time
if usec, err := strconv.ParseInt(tsStr, 10, 64); err == nil {
entryTime = time.Unix(0, usec*int64(time.Microsecond))
} else {
entryTime = time.Now()
}
logChan <- analysisServices.Entry{
Message: raw.Message,
Unit: raw.Unit,
PID: raw.PID,
Time: entryTime,
}
}
return cmd.Wait()
}
func (s *systemd) Close() error {
if s.units == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.cmd != nil && s.cmd.Process != nil {
s.logger.Debug("Stopping journalctl")
// Force journalctl to quit on shutdown
return s.cmd.Process.Kill()
}
s.logger.Debug("Journalctl stopped")
return nil
}

View File

@@ -5,7 +5,10 @@ import (
"errors"
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/pidfile"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/socket"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
@@ -17,10 +20,13 @@ type Daemon interface {
}
type daemon struct {
pidFile pidfile.PidFile
socket socket.Socket
logger log.Logger
firewall firewall.API
pidFile pidfile.PidFile
socket socket.Socket
logger log.Logger
firewall firewall.API
notifications notifications.Notifications
analyzer analyzer.Analyzer
docker docker_monitor.Docker
stopCh chan struct{}
}
@@ -52,6 +58,23 @@ func (d *daemon) Run(ctx context.Context, isTesting bool, testingInterval uint16
_ = d.socket.Close()
}()
d.notifications.Run()
defer func() {
_ = d.notifications.Close()
}()
d.analyzer.Run(ctx)
defer func() {
_ = d.analyzer.Close()
}()
if d.firewall.DockerSupport() {
go d.docker.Run()
defer func() {
_ = d.docker.Close()
}()
}
go d.socket.Run(ctx, d.socketCommand)
d.runWorker(ctx, isTesting, testingInterval)

View File

@@ -0,0 +1,79 @@
package chain
import nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
type Chains interface {
ForwardFilterJump(addRule func(expr ...string) error) error
PreroutingFilterJump(addRule func(expr ...string) error) error
PreroutingNatJump(addRule func(expr ...string) error) error
OutputNatJump(addRule func(expr ...string) error) error
PostroutingNatJump(addRule func(expr ...string) error) error
List() *chains
}
type chains struct {
ForwardFilter Data
ForwardBridge Data
ForwardCT Data
PreroutingFilter Data
DockerFilter Data
DockerFilterFirst Data
DockerFilterSecond Data
DockerNat Data
PostroutingNat Data
}
type Data struct {
chain nftChain.Chain
name string
}
func (d *chains) ForwardFilterJump(addRule func(expr ...string) error) error {
return d.ForwardFilter.Jump(addRule, "")
}
func (d *chains) PreroutingFilterJump(addRule func(expr ...string) error) error {
return d.PreroutingFilter.Jump(addRule, "")
}
func (d *chains) PreroutingNatJump(addRule func(expr ...string) error) error {
return d.DockerNat.Jump(addRule, "fib daddr type local counter")
}
func (d *chains) OutputNatJump(addRule func(expr ...string) error) error {
if err := d.DockerNat.Jump(addRule, "ip daddr != 127.0.0.0/8 fib daddr type local counter"); err != nil {
return err
}
return d.DockerNat.Jump(addRule, "ip6 daddr != ::1 fib daddr type local counter")
}
func (d *chains) PostroutingNatJump(addRule func(expr ...string) error) error {
return d.PostroutingNat.Jump(addRule, "")
}
func (d *chains) List() *chains {
return d
}
func (d *Data) Jump(addRule func(expr ...string) error, rule string) error {
args := []string{rule, "jump", d.name}
return addRule(args...)
}
func (d *Data) JumpTo(data *Data, rule string) error {
args := []string{rule, "jump", d.name}
return data.AddRule(args...)
}
func (d *Data) AddRule(rule ...string) error {
return d.chain.AddRule(rule...)
}
func (d *Data) Clear() error {
return d.chain.Clear()
}

View File

@@ -0,0 +1,32 @@
package chain
type emptyChains struct {
}
func NewEmptyChains() Chains {
return &emptyChains{}
}
func (c *emptyChains) ForwardFilterJump(_ func(expr ...string) error) error {
return nil
}
func (c *emptyChains) PreroutingFilterJump(_ func(expr ...string) error) error {
return nil
}
func (c *emptyChains) PreroutingNatJump(_ func(expr ...string) error) error {
return nil
}
func (c *emptyChains) OutputNatJump(_ func(expr ...string) error) error {
return nil
}
func (c *emptyChains) PostroutingNatJump(_ func(expr ...string) error) error {
return nil
}
func (c *emptyChains) List() *chains {
return &chains{}
}

View File

@@ -0,0 +1,77 @@
package chain
import nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
func NewChains(newNoneChain func(chain string) (nftChain.Chain, error)) (Chains, error) {
chainsData := &chains{}
if data, err := newChainData("docker_nat", newNoneChain); err != nil {
return nil, err
} else {
chainsData.DockerNat = data
}
if data, err := newChainData("docker_postrouting_nat", newNoneChain); err != nil {
return nil, err
} else {
chainsData.PostroutingNat = data
}
if data, err := newChainData("docker_prerouting_filter", newNoneChain); err != nil {
return nil, err
} else {
chainsData.PreroutingFilter = data
}
if data, err := newChainData("docker_filter", newNoneChain); err != nil {
return nil, err
} else {
chainsData.DockerFilter = data
}
if data, err := newChainData("docker_filter_first", newNoneChain); err != nil {
return nil, err
} else {
chainsData.DockerFilterFirst = data
}
if data, err := newChainData("docker_filter_second", newNoneChain); err != nil {
return nil, err
} else {
chainsData.DockerFilterSecond = data
}
if data, err := newChainData("docker_forward_filter", newNoneChain); err != nil {
return nil, err
} else {
chainsData.ForwardFilter = data
}
if data, err := newChainData("docker_forward_bridge", newNoneChain); err != nil {
return nil, err
} else {
chainsData.ForwardBridge = data
}
if data, err := newChainData("docker_forward_ct", newNoneChain); err != nil {
return nil, err
} else {
chainsData.ForwardCT = data
}
return chainsData, nil
}
func newChainData(chainName string, newNoneChain func(chain string) (nftChain.Chain, error)) (Data, error) {
data := Data{
name: chainName,
}
newChain, err := newNoneChain(data.name)
if err != nil {
return data, err
}
data.chain = newChain
return data, nil
}

View File

@@ -0,0 +1,66 @@
package client
import (
"fmt"
"strings"
)
func (d *docker) Bridges() ([]string, error) {
args := []string{"network", "ls", "-q", "--filter", "Driver=bridge"}
result, err := d.command(args...)
if err != nil {
return nil, fmt.Errorf("failed to get docker bridge names: %s", err.Error())
}
output := strings.TrimSpace(string(result))
if output == "" {
return []string{}, nil
}
lines := strings.Split(output, "\n")
for i := range lines {
lines[i] = strings.TrimSpace(lines[i])
}
return lines, nil
}
func (d *docker) BridgeNames() ([]string, error) {
bridges, err := d.Bridges()
if err != nil {
return nil, err
}
var names []string
for _, bridge := range bridges {
bridgeName, err := d.BridgeName(bridge)
if err != nil {
d.logger.Error(err.Error())
continue
}
names = append(names, bridgeName)
}
return names, nil
}
func (d *docker) BridgeName(bridgeID string) (string, error) {
format := fmt.Sprintf(`{{"br-%s" | or (index .Options "com.docker.network.bridge.name")}}`, bridgeID)
args := []string{"network", "inspect", "-f", format, bridgeID}
result, err := d.command(args...)
if err != nil {
return "", fmt.Errorf("failed to get bridge name: %s", err.Error())
}
return strings.TrimSpace(string(result)), nil
}
func (d *docker) BridgeSubnet(bridgeID string) (string, error) {
format := fmt.Sprintf(`{{range .IPAM.Config}}{{.Subnet}}{{end}}`)
args := []string{"network", "inspect", "-f", format, bridgeID}
result, err := d.command(args...)
if err != nil {
return "", fmt.Errorf("failed to get bridge subnet: %s", err.Error())
}
return strings.TrimSpace(string(result)), nil
}

View File

@@ -0,0 +1,84 @@
package client
import (
"encoding/json"
"fmt"
"strings"
)
func (d *docker) Containers(bridgeID string) ([]string, error) {
args := []string{"ps", "-q", "--filter", fmt.Sprintf("network=%s", bridgeID)}
result, err := d.command(args...)
if err != nil {
return nil, fmt.Errorf("failed to get docker containers: %s", err.Error())
}
output := strings.TrimSpace(string(result))
if output == "" {
return []string{}, nil
}
lines := strings.Split(output, "\n")
for i := range lines {
lines[i] = strings.TrimSpace(lines[i])
}
return lines, nil
}
func (d *docker) ContainerNetworks(containerID string) (DockerContainerInspect, error) {
result, err := d.command("inspect", containerID)
if err != nil {
return DockerContainerInspect{}, err
}
var info []DockerContainerInspect
if err := json.Unmarshal(result, &info); err != nil {
return DockerContainerInspect{}, err
}
if len(info) == 0 {
return DockerContainerInspect{}, fmt.Errorf("container %s not found", containerID)
}
return info[0], nil
}
func (d *docker) parsePorts(info DockerContainerInspect) []ContainerPort {
var ports []ContainerPort
for containerPortFull, hostConfigs := range info.NetworkSettings.Ports {
parts := strings.Split(containerPortFull, "/")
portNum := parts[0]
protocol := "tcp" // default
if len(parts) > 1 {
protocol = parts[1]
}
cp := ContainerPort{
Port: portNum,
Protocol: protocol,
}
for _, h := range hostConfigs {
host := HostPort{
Port: h.HostPort,
}
ipVersion, err := ipVersion(h.HostIp)
if err != nil {
d.logger.Error(err.Error())
continue
}
host.IP = IPInfo{Address: h.HostIp, Version: ipVersion}
cp.HostPort = append(cp.HostPort, host)
}
ports = append(ports, cp)
}
return ports
}

View File

@@ -0,0 +1,158 @@
package client
import (
"bufio"
"context"
"fmt"
"os/exec"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Docker interface {
FetchBridges() (Bridges, error)
FetchContainers(bridgeID string) (Containers, error)
Bridges() ([]string, error)
BridgeNames() ([]string, error)
BridgeName(bridgeID string) (string, error)
BridgeSubnet(bridgeID string) (string, error)
Containers(bridgeID string) ([]string, error)
ContainerNetworks(containerID string) (DockerContainerInspect, error)
Events() (<-chan string, <-chan error)
}
type docker struct {
path string
ctx context.Context
logger log.Logger
}
func NewDocker(path string, ctx context.Context, logger log.Logger) Docker {
return &docker{
path: path,
ctx: ctx,
logger: logger,
}
}
func (d *docker) FetchBridges() (Bridges, error) {
bridges := Bridges{}
list, err := d.Bridges()
if err != nil {
return nil, err
}
for _, bridgeId := range list {
bridgeName, err := d.BridgeName(bridgeId)
if err != nil {
d.logger.Error(err.Error())
continue
}
bridgeSubnet, err := d.BridgeSubnet(bridgeId)
if err != nil {
d.logger.Error(err.Error())
continue
}
var containers Containers
containers, err = d.FetchContainers(bridgeId)
if err != nil {
d.logger.Error(err.Error())
}
bridges = append(bridges, Bridge{
ID: bridgeId,
Name: bridgeName,
Subnet: bridgeSubnet,
Containers: containers,
})
}
return bridges, nil
}
func (d *docker) FetchContainers(bridgeID string) (Containers, error) {
containers := Containers{}
list, err := d.Containers(bridgeID)
if err != nil {
return nil, err
}
for _, containerID := range list {
info, err := d.ContainerNetworks(containerID)
if err != nil {
d.logger.Error(err.Error())
continue
}
networks := ContainerNetworks{
IPAddresses: []IPInfo{},
Ports: d.parsePorts(info),
}
for _, networkData := range info.NetworkSettings.Networks {
if networkData.IPAddress != "" {
ipVesion, err := ipVersion(networkData.IPAddress)
if err != nil {
d.logger.Error(err.Error())
continue
}
networks.IPAddresses = append(networks.IPAddresses, IPInfo{Address: networkData.IPAddress, Version: ipVesion})
}
}
containers = append(containers, Container{
ID: containerID,
Networks: networks,
})
}
return containers, nil
}
func (d *docker) command(args ...string) ([]byte, error) {
cmd := exec.CommandContext(d.ctx, d.path, args...)
result, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf(string(result))
}
return result, nil
}
func (d *docker) Events() (<-chan string, <-chan error) {
eventsChan := make(chan string)
errChan := make(chan error)
go func() {
defer close(eventsChan)
defer close(errChan)
args := []string{
"events",
"--filter", "type=container",
"--filter", "event=start",
"--filter", "event=die",
"--format",
"{{json .}}",
}
cmd := exec.CommandContext(d.ctx, "docker", args...)
stdout, err := cmd.StdoutPipe()
if err != nil {
errChan <- err
return
}
if err := cmd.Start(); err != nil {
errChan <- err
return
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
eventsChan <- scanner.Text()
}
}()
return eventsChan, errChan
}

View File

@@ -0,0 +1,75 @@
package client
import (
"errors"
"net"
)
type Bridges []Bridge
type Bridge struct {
ID string
Name string
Subnet string
Containers Containers
}
type Containers []Container
type Container struct {
ID string
Networks ContainerNetworks
}
type ContainerNetworks struct {
IPAddresses []IPInfo
Ports []ContainerPort
}
type IPInfo struct {
Address string
Version int // "4" or "6"
}
func (i IPInfo) NftPrefix() string {
if i.Version == 6 {
return "ip6"
}
return "ip"
}
type ContainerPort struct {
Port string
Protocol string
HostPort []HostPort
}
type HostPort struct {
Port string
IP IPInfo
}
type DockerContainerInspect struct {
NetworkSettings struct {
Ports map[string][]struct {
HostIp string `json:"HostIp"`
HostPort string `json:"HostPort"`
} `json:"Ports"`
Networks map[string]struct {
IPAddress string `json:"IPAddress"`
} `json:"Networks"`
} `json:"NetworkSettings"`
}
func ipVersion(ip string) (int, error) {
ipParse := net.ParseIP(ip)
if ipParse == nil || (ipParse.To4() == nil && ipParse.To16() == nil) {
return 0, errors.New("invalid ip address")
}
if ipParse.To4() != nil {
return 4, nil
}
return 6, nil
}

View File

@@ -0,0 +1,188 @@
package docker_monitor
import (
"context"
"fmt"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/client"
nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Docker interface {
NftReload(newNoneChain func(chain string) (nftChain.Chain, error)) error
NftChains() chain.Chains
Run()
Close() error
}
type docker struct {
client client.Docker
logger log.Logger
ctx context.Context
chains chain.Chains
}
func New(path string, ctx context.Context, logger log.Logger) Docker {
return &docker{
client: client.NewDocker(path, ctx, logger),
logger: logger,
ctx: ctx,
}
}
func (d *docker) NftReload(newNoneChain func(chain string) (nftChain.Chain, error)) error {
chains, err := chain.NewChains(newNoneChain)
if err != nil {
return err
}
d.chains = chains
d.nftRuleReload()
return nil
}
func (d *docker) NftChains() chain.Chains {
return d.chains
}
func (d *docker) Run() {
events, errs := d.client.Events()
for {
select {
case <-d.ctx.Done():
return
case msg := <-events:
d.logger.Debug("Docker event received: " + msg)
// TODO: A temporary solution to test how it will interact with nftables in a production environment
listChains := d.NftChains().List()
if err := listChains.DockerNat.Clear(); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.PostroutingNat.Clear(); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.PreroutingFilter.Clear(); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.DockerFilter.Clear(); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.DockerFilterFirst.Clear(); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.DockerFilterSecond.Clear(); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.ForwardFilter.Clear(); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.ForwardBridge.Clear(); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.ForwardCT.Clear(); err != nil {
d.logger.Error(err.Error())
}
d.nftRuleReload()
case err := <-errs:
d.logger.Error("Docker events error: " + err.Error())
}
}
}
func (d *docker) Close() error {
return nil
}
func (d *docker) chainCommand(chainData chain.Data, rule string) {
if err := chainData.AddRule(rule); err != nil {
d.logger.Error(err.Error())
}
}
func (d *docker) nftRuleReload() {
listChains := d.NftChains().List()
if err := listChains.ForwardCT.JumpTo(&listChains.ForwardFilter, ""); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.ForwardBridge.JumpTo(&listChains.ForwardFilter, ""); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.DockerFilterFirst.JumpTo(&listChains.DockerFilter, ""); err != nil {
d.logger.Error(err.Error())
}
if err := listChains.DockerFilterSecond.JumpTo(&listChains.DockerFilter, ""); err != nil {
d.logger.Error(err.Error())
}
bridges, err := d.client.FetchBridges()
if err != nil {
d.logger.Error(err.Error())
return
}
var rule string
for _, bridge := range bridges {
rule = fmt.Sprintf("iifname != \"%s\" oifname \"%s\" counter drop", bridge.Name, bridge.Name)
d.chainCommand(listChains.DockerFilterSecond, rule)
rule = fmt.Sprintf("iifname \"%s\" counter accept", bridge.Name)
d.chainCommand(listChains.ForwardFilter, rule)
rule = fmt.Sprintf("oifname \"%s\" counter", bridge.Name)
if err := listChains.DockerFilter.JumpTo(&listChains.ForwardBridge, rule); err != nil {
d.logger.Error(err.Error())
}
rule = fmt.Sprintf("oifname \"%s\" ct state related,established counter accept", bridge.Name)
d.chainCommand(listChains.ForwardCT, rule)
rule = fmt.Sprintf("ip saddr %s oifname != \"%s\" counter masquerade", bridge.Subnet, bridge.Name)
d.chainCommand(listChains.PostroutingNat, rule)
if bridge.Containers == nil {
continue
}
for _, container := range bridge.Containers {
for _, ipInfo := range container.Networks.IPAddresses {
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" counter drop", ipInfo.NftPrefix(), ipInfo.Address, bridge.Name)
d.chainCommand(listChains.PreroutingFilter, rule)
for _, port := range container.Networks.Ports {
isZeroAddress := false
for _, hostInfo := range port.HostPort {
if hostInfo.IP.Address != "0.0.0.0" && hostInfo.IP.Address != "::" && (hostInfo.IP.Address == "127.0.0.1" || hostInfo.IP.Address == "::1") {
rule = fmt.Sprintf("%s daddr %s iifname != \"lo\" %s dport %s counter drop", hostInfo.IP.NftPrefix(), hostInfo.IP.Address, port.Protocol, hostInfo.Port)
d.chainCommand(listChains.PreroutingFilter, rule)
}
if hostInfo.IP.Address == "0.0.0.0" || hostInfo.IP.Address == "::" {
if isZeroAddress {
continue
}
isZeroAddress = true
rule = fmt.Sprintf("iifname != \"%s\" %s dport %s counter dnat %s to %s:%s", bridge.Name, port.Protocol, hostInfo.Port, ipInfo.NftPrefix(), ipInfo.Address, port.Port)
d.chainCommand(listChains.DockerNat, rule)
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" oifname \"%s\" %s dport %s counter accept", ipInfo.NftPrefix(), ipInfo.Address, bridge.Name, bridge.Name, port.Protocol, port.Port)
d.chainCommand(listChains.DockerFilterFirst, rule)
continue
}
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" oifname \"%s\" %s dport %s counter accept", ipInfo.NftPrefix(), ipInfo.Address, bridge.Name, bridge.Name, port.Protocol, port.Port)
d.chainCommand(listChains.DockerFilterFirst, rule)
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" %s dport %s counter dnat to %s:%s", hostInfo.IP.NftPrefix(), hostInfo.IP.Address, bridge.Name, port.Protocol, hostInfo.Port, ipInfo.Address, port.Port)
d.chainCommand(listChains.DockerNat, rule)
}
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
package docker_monitor
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/chain"
nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
)
type DockerNotSupport struct {
chains chain.Chains
}
func NewDockerNotSupport() Docker {
return &DockerNotSupport{
chains: chain.NewEmptyChains(),
}
}
func (d *DockerNotSupport) NftReload(_ func(chain string) (nftChain.Chain, error)) error {
return nil
}
func (d *DockerNotSupport) NftChains() chain.Chains {
return d.chains
}
func (d *DockerNotSupport) Run() {
}
func (d *DockerNotSupport) Close() error {
return nil
}

View File

@@ -0,0 +1,26 @@
package chain
import (
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
)
type Chain interface {
AddRule(expr ...string) error
Clear() error
}
type chain struct {
nft nft.NFT
family family.Type
table string
chain string
}
func (c *chain) AddRule(expr ...string) error {
return c.nft.Rule().Add(c.family, c.table, c.chain, expr...)
}
func (c *chain) Clear() error {
return c.nft.Chain().Clear(c.family, c.table, c.chain)
}

View File

@@ -1,7 +1,10 @@
package chain
import (
"strings"
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
nftFamily "git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
)
@@ -9,13 +12,13 @@ type Chains interface {
NewPacketFilter(enable bool) error
PacketFilter() PacketFilter
NewInput(chain string, defaultAllow bool) error
NewInput(chain string, defaultAllow bool, priority int) error
Input() Input
NewOutput(chain string, defaultAllow bool) error
NewOutput(chain string, defaultAllow bool, priority int) error
Output() Output
NewForward(chain string, defaultAllow bool) error
NewForward(chain string, defaultAllow bool, priority int) error
Forward() Forward
NewLocalInput() error
@@ -23,6 +26,11 @@ type Chains interface {
NewLocalOutput() error
LocalOutput() LocalOutput
ClearRules() error
NewNoneChain(chain string) (Chain, error)
NewChain(chain string, baseChain nftChain.ChainOptions) (Chain, error)
}
type chains struct {
@@ -40,11 +48,12 @@ type chains struct {
}
func NewChains(nft nft.NFT, table string) (Chains, error) {
if err := nft.Clear(); err != nil {
family := nftFamily.INET
if err := clearRules(nft, family, table); err != nil {
return nil, err
}
family := nftFamily.INET
if err := nft.Table().Add(family, table); err != nil {
return nil, err
}
@@ -70,8 +79,8 @@ func (c *chains) PacketFilter() PacketFilter {
return c.packetFilter
}
func (c *chains) NewInput(chain string, defaultAllow bool) error {
input, err := newInput(c.nft, c.family, c.table, chain, defaultAllow)
func (c *chains) NewInput(chain string, defaultAllow bool, priority int) error {
input, err := newInput(c.nft, c.family, c.table, chain, defaultAllow, priority)
if err != nil {
return err
}
@@ -84,8 +93,8 @@ func (c *chains) Input() Input {
return c.input
}
func (c *chains) NewOutput(chain string, defaultAllow bool) error {
output, err := newOutput(c.nft, c.family, c.table, chain, defaultAllow)
func (c *chains) NewOutput(chain string, defaultAllow bool, priority int) error {
output, err := newOutput(c.nft, c.family, c.table, chain, defaultAllow, priority)
if err != nil {
return err
}
@@ -98,8 +107,8 @@ func (c *chains) Output() Output {
return c.output
}
func (c *chains) NewForward(chain string, defaultAllow bool) error {
forward, err := newForward(c.nft, c.family, c.table, chain, defaultAllow)
func (c *chains) NewForward(chain string, defaultAllow bool, priority int) error {
forward, err := newForward(c.nft, c.family, c.table, chain, defaultAllow, priority)
if err != nil {
return err
}
@@ -137,3 +146,34 @@ func (c *chains) NewLocalOutput() error {
func (c *chains) LocalOutput() LocalOutput {
return c.localOutput
}
func (c *chains) ClearRules() error {
return clearRules(c.nft, c.family, c.table)
}
func (c *chains) NewNoneChain(chainName string) (Chain, error) {
return c.NewChain(chainName, nftChain.TypeNone)
}
func (c *chains) NewChain(chainName string, baseChain nftChain.ChainOptions) (Chain, error) {
if err := c.nft.Chain().Add(c.family, c.table, chainName, baseChain); err != nil {
return nil, err
}
return &chain{
nft: c.nft,
family: c.family,
table: c.table,
chain: chainName,
}, nil
}
func clearRules(nft nft.NFT, family nftFamily.Type, table string) error {
if err := nft.Table().Delete(family, table); err != nil {
if !strings.Contains(string(err.Error()), "delete table "+family.String()+" "+table) {
return err
}
}
return nil
}

View File

@@ -17,7 +17,7 @@ type forward struct {
chain string
}
func newForward(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool) (Forward, error) {
func newForward(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool, priority int) (Forward, error) {
policy := nftChain.PolicyDrop
if defaultAllow {
policy = nftChain.PolicyAccept
@@ -26,7 +26,7 @@ func newForward(nft nft.NFT, family family.Type, table string, chain string, def
baseChain := nftChain.BaseChainOptions{
Type: nftChain.TypeFilter,
Hook: nftChain.HookForward,
Priority: 0,
Priority: int32(priority),
Policy: policy,
Device: "",
}

View File

@@ -17,7 +17,7 @@ type input struct {
chain string
}
func newInput(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool) (Input, error) {
func newInput(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool, priority int) (Input, error) {
policy := nftChain.PolicyDrop
if defaultAllow {
policy = nftChain.PolicyAccept
@@ -26,7 +26,7 @@ func newInput(nft nft.NFT, family family.Type, table string, chain string, defau
baseChain := nftChain.BaseChainOptions{
Type: nftChain.TypeFilter,
Hook: nftChain.HookInput,
Priority: 0,
Priority: int32(priority),
Policy: policy,
Device: "",
}

View File

@@ -32,10 +32,10 @@ func newLocalInput(nft nft.NFT, family family.Type, table string) (LocalInput, e
}, nil
}
func (c *localInput) AddRule(expr ...string) error {
return c.nft.Rule().Add(c.family, c.table, c.chain, expr...)
func (l *localInput) AddRule(expr ...string) error {
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
}
func (f *localInput) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
return AddRuleFunc("iifname != \"lo\" counter jump " + f.chain)
func (l *localInput) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
return AddRuleFunc("iifname != \"lo\" counter jump " + l.chain)
}

View File

@@ -32,10 +32,10 @@ func newLocalOutput(nft nft.NFT, family family.Type, table string) (LocalOutput,
}, nil
}
func (c *localOutput) AddRule(expr ...string) error {
return c.nft.Rule().Add(c.family, c.table, c.chain, expr...)
func (l *localOutput) AddRule(expr ...string) error {
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
}
func (f *localOutput) AddRuleOut(AddRuleFunc func(expr ...string) error) error {
return AddRuleFunc("oifname != \"lo\" counter jump " + f.chain)
func (l *localOutput) AddRuleOut(AddRuleFunc func(expr ...string) error) error {
return AddRuleFunc("oifname != \"lo\" counter jump " + l.chain)
}

View File

@@ -17,7 +17,7 @@ type output struct {
chain string
}
func newOutput(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool) (Output, error) {
func newOutput(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool, priority int) (Output, error) {
policy := nftChain.PolicyDrop
if defaultAllow {
policy = nftChain.PolicyAccept
@@ -26,7 +26,7 @@ func newOutput(nft nft.NFT, family family.Type, table string, chain string, defa
baseChain := nftChain.BaseChainOptions{
Type: nftChain.TypeFilter,
Hook: nftChain.HookOutput,
Priority: 0,
Priority: int32(priority),
Policy: policy,
Device: "",
}

View File

@@ -1,6 +1,8 @@
package firewall
import "fmt"
import (
"fmt"
)
type Config struct {
InPorts []ConfigPort
@@ -13,11 +15,13 @@ type Config struct {
}
type ConfigOptions struct {
ClearMode ClearMode
SavesRules bool
SavesRulesPath string
DnsStrict bool
DnsStrictNs bool
PacketFilter bool
DockerSupport bool
}
type ConfigMetadata struct {
@@ -32,8 +36,11 @@ type ConfigPolicy struct {
DefaultAllowOutput bool
DefaultAllowForward bool
InputDrop PolicyDrop
InputPriority int
OutputDrop PolicyDrop
OutputPriority int
ForwardDrop PolicyDrop
ForwardPriority int
}
type PolicyDrop int8
@@ -143,3 +150,10 @@ func (d Direction) String() string {
return fmt.Sprintf("Direction(%d)", d)
}
}
type ClearMode int8
const (
ClearModeGlobal ClearMode = iota + 1
ClearModeOwn
)

View File

@@ -0,0 +1,72 @@
package firewall
import nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
func (f *firewall) reloadDocker() error {
f.logger.Debug("Reload docker rules")
if err := f.reloadDockerPrerouting(); err != nil {
return err
}
return nil
}
func (f *firewall) reloadDockerPrerouting() error {
preroutingNat, err := f.chains.NewChain("prerouting_nat", nftChain.BaseChainOptions{
Type: nftChain.TypeNat,
Hook: nftChain.HookPrerouting,
Priority: -100,
Policy: nftChain.PolicyAccept,
Device: "",
})
if err != nil {
return err
}
if err := f.docker.NftChains().PreroutingNatJump(preroutingNat.AddRule); err != nil {
return err
}
preroutingFilter, err := f.chains.NewChain("prerouting_filter", nftChain.BaseChainOptions{
Type: nftChain.TypeFilter,
Hook: nftChain.HookPrerouting,
Priority: -300,
Policy: nftChain.PolicyAccept,
Device: "",
})
if err != nil {
return err
}
if err := f.docker.NftChains().PreroutingFilterJump(preroutingFilter.AddRule); err != nil {
return err
}
outputNat, err := f.chains.NewChain("output_nat", nftChain.BaseChainOptions{
Type: nftChain.TypeNat,
Hook: nftChain.HookOutput,
Priority: -100,
Policy: nftChain.PolicyAccept,
Device: "",
})
if err != nil {
return err
}
if err := f.docker.NftChains().OutputNatJump(outputNat.AddRule); err != nil {
return err
}
postroutingNat, err := f.chains.NewChain("postrouting_nat", nftChain.BaseChainOptions{
Type: nftChain.TypeNat,
Hook: nftChain.HookPostrouting,
Priority: 300,
Policy: nftChain.PolicyAccept,
Device: "",
})
if err != nil {
return err
}
if err := f.docker.NftChains().PostroutingNatJump(postroutingNat.AddRule); err != nil {
return err
}
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
@@ -19,6 +20,8 @@ type API interface {
// ClearRules Clear all rules.
ClearRules()
DockerSupport() bool
}
type firewall struct {
@@ -26,9 +29,10 @@ type firewall struct {
logger log.Logger
config *Config
chains chain.Chains
docker docker_monitor.Docker
}
func New(pathNFT string, logger log.Logger, config Config) (API, error) {
func New(pathNFT string, logger log.Logger, config Config, docker docker_monitor.Docker) (API, error) {
nft, err := nftables.NewWithPath(pathNFT)
if err != nil {
return nil, fmt.Errorf("failed to create nft client: %w %s", err, pathNFT)
@@ -38,16 +42,28 @@ func New(pathNFT string, logger log.Logger, config Config) (API, error) {
nft: nft,
logger: logger,
config: &config,
docker: docker,
}, nil
}
func (f *firewall) Reload() error {
f.logger.Debug("Reload nftables rules")
if f.config.Options.ClearMode == ClearModeGlobal {
if err := f.nft.Clear(); err != nil {
return err
}
}
chains, err := chain.NewChains(f.nft, f.config.MetadataNaming.TableName)
if err != nil {
return err
}
f.chains = chains
if err := f.docker.NftReload(f.chains.NewNoneChain); err != nil {
return err
}
if err := f.chains.NewPacketFilter(f.config.Options.PacketFilter); err != nil {
return err
}
@@ -60,6 +76,11 @@ func (f *firewall) Reload() error {
if err := f.reloadForward(); err != nil {
return err
}
if f.config.Options.DockerSupport {
if err := f.reloadDocker(); err != nil {
return err
}
}
f.logger.Debug("Reload nftables rules done")
return nil
@@ -67,9 +88,20 @@ func (f *firewall) Reload() error {
func (f *firewall) ClearRules() {
f.logger.Debug("Clear nftables rules")
if err := f.nft.Clear(); err != nil {
f.logger.Error(fmt.Sprintf("Failed to clear rules: %s", err))
switch f.config.Options.ClearMode {
case ClearModeGlobal:
if err := f.nft.Clear(); err != nil {
f.logger.Error(fmt.Sprintf("Failed to clear rules: %s", err))
}
break
case ClearModeOwn:
if err := f.chains.ClearRules(); err != nil {
f.logger.Error(fmt.Sprintf("Failed to clear rules: %s", err))
}
break
}
f.logger.Debug("Clear nftables rules done")
}
@@ -100,3 +132,7 @@ func (f *firewall) SavesRules() {
f.logger.Info("Save nftables rules")
}
func (f *firewall) DockerSupport() bool {
return f.config.Options.DockerSupport
}

View File

@@ -2,12 +2,18 @@ package firewall
func (f *firewall) reloadForward() error {
f.logger.Debug("Reloading forward chain")
err := f.chains.NewForward(f.config.MetadataNaming.ChainForwardName, f.config.Policy.DefaultAllowForward)
err := f.chains.NewForward(f.config.MetadataNaming.ChainForwardName, f.config.Policy.DefaultAllowForward, f.config.Policy.ForwardPriority)
if err != nil {
return err
}
chain := f.chains.Forward()
if f.config.Options.DockerSupport {
if err := f.docker.NftChains().ForwardFilterJump(chain.AddRule); err != nil {
return err
}
}
if f.config.Policy.DefaultAllowForward == false {
drop := f.config.Policy.ForwardDrop.String()
if err := chain.AddRule(drop); err != nil {

View File

@@ -10,7 +10,7 @@ import (
func (f *firewall) reloadInput() error {
f.logger.Debug("Reloading input chain")
err := f.chains.NewInput(f.config.MetadataNaming.ChainInputName, f.config.Policy.DefaultAllowInput)
err := f.chains.NewInput(f.config.MetadataNaming.ChainInputName, f.config.Policy.DefaultAllowInput, f.config.Policy.InputPriority)
if err != nil {
return err
}

View File

@@ -10,7 +10,7 @@ import (
func (f *firewall) reloadOutput() error {
f.logger.Debug("Reloading output chain")
err := f.chains.NewOutput(f.config.MetadataNaming.ChainOutputName, f.config.Policy.DefaultAllowOutput)
err := f.chains.NewOutput(f.config.MetadataNaming.ChainOutputName, f.config.Policy.DefaultAllowOutput, f.config.Policy.OutputPriority)
if err != nil {
return err
}

View File

@@ -0,0 +1,43 @@
package notifications
import (
"github.com/wneessen/go-mail"
)
type Config struct {
Enabled bool
ServerName string
Email Email
}
type Email struct {
Host string
Port uint
Username string
Password string
AuthType mail.SMTPAuthType
TLS TLS
From string
To string
}
type TLS struct {
Mode TLSMode
Policy TLSPolicy
Verify bool
}
type TLSMode string
const (
TLSModeNone TLSMode = "NONE"
TLSModeStartTLS TLSMode = "STARTTLS"
TLSModeImplicit TLSMode = "IMPLICIT"
)
type TLSPolicy string
const (
TLSPolicyMandatory TLSPolicy = "MANDATORY"
TLSPolicyOpportunistic TLSPolicy = "OPPORTUNISTIC"
)

View File

@@ -0,0 +1,151 @@
package notifications
import (
"context"
"crypto/tls"
"fmt"
"sync"
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
"github.com/wneessen/go-mail"
)
type Message struct {
Subject string
Body string
}
type Notifications interface {
Run()
SendAsync(message Message)
Close() error
}
type notifications struct {
config Config
logger log.Logger
msgQueue chan Message
wg sync.WaitGroup
}
func New(config Config, logger log.Logger) Notifications {
return &notifications{
config: config,
logger: logger,
msgQueue: make(chan Message, 100),
}
}
func (n *notifications) Run() {
if n.config.Enabled == false {
n.logger.Info("Notifications are disabled")
}
n.wg.Add(1)
go func() {
defer n.wg.Done()
for msg := range n.msgQueue {
err := n.sendEmail(msg)
if err != nil {
n.logger.Error(fmt.Sprintf("failed to send email: %v", err))
} else if n.config.Enabled {
n.logger.Debug(fmt.Sprintf("email sent: Subject %s, Body %s", msg.Subject, msg.Body))
}
}
}()
}
func (n *notifications) SendAsync(message Message) {
select {
case n.msgQueue <- message:
if n.config.Enabled == false {
n.logger.Debug(fmt.Sprintf("email sending is disabled, message was added to the queue: Subject %s, Body %s", message.Subject, message.Body))
} else {
n.logger.Debug(fmt.Sprintf("added to the mail sending queue: Subject %s, Body %s", message.Subject, message.Body))
}
default:
n.logger.Error(fmt.Sprintf("failed to send email: queue is full"))
}
}
func (n *notifications) Close() error {
close(n.msgQueue)
n.logger.Debug("We are waiting for all notifications to be sent")
n.wg.Wait()
n.logger.Debug("Notifications queue processed and closed")
return nil
}
func (n *notifications) sendEmail(message Message) error {
if n.config.Enabled == false {
return nil
}
m := mail.NewMsg()
if err := m.From(n.config.Email.From); err != nil {
return err
}
if err := m.To(n.config.Email.To); err != nil {
return err
}
m.Subject(message.Subject + " (" + n.config.ServerName + ")")
m.SetBodyString(mail.TypeTextPlain, "Server: "+n.config.ServerName+"\n"+message.Body)
client, err := newClient(n.config.Email)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return client.DialAndSendWithContext(ctx, m)
}
func newClient(config Email) (*mail.Client, error) {
options := []mail.Option{
mail.WithPort(int(config.Port)),
mail.WithSMTPAuth(config.AuthType),
}
if config.AuthType != mail.SMTPAuthNoAuth {
options = append(options, mail.WithUsername(config.Username), mail.WithPassword(config.Password))
}
switch config.TLS.Mode {
case TLSModeImplicit:
options = append(options, mail.WithSSL())
break
case TLSModeStartTLS:
switch config.TLS.Policy {
case TLSPolicyMandatory:
options = append(options, mail.WithTLSPolicy(mail.TLSMandatory))
break
case TLSPolicyOpportunistic:
options = append(options, mail.WithTLSPolicy(mail.TLSOpportunistic))
break
default:
return nil, fmt.Errorf("unknown tls policy: %s", config.TLS.Policy)
}
if !config.TLS.Verify {
tlsCfg := &tls.Config{
InsecureSkipVerify: true,
}
options = append(options, mail.WithTLSConfig(tlsCfg))
}
break
case TLSModeNone:
break
default:
return nil, fmt.Errorf("unknown tls mode: %s", config.TLS.Mode)
}
options = append(options, mail.WithSSL())
return mail.NewClient(config.Host, options...)
}

View File

@@ -1,10 +1,14 @@
package daemon
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
)
type DaemonOptions struct {
PathPidFile string
PathSocketFile string
PathNftables string
ConfigFirewall firewall.Config
ConfigAnalyzer config.Config
}

View File

@@ -3,13 +3,16 @@ package daemon
import (
"errors"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
firewall2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/pidfile"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/socket"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
func NewDaemon(opts DaemonOptions, logger log.Logger) (Daemon, error) {
func NewDaemon(opts DaemonOptions, logger log.Logger, notifications notifications.Notifications, docker docker_monitor.Docker) (Daemon, error) {
if logger == nil {
return nil, errors.New("logger is nil")
}
@@ -24,12 +27,17 @@ func NewDaemon(opts DaemonOptions, logger log.Logger) (Daemon, error) {
return nil, err
}
firewall, err := firewall2.New(opts.PathNftables, logger, opts.ConfigFirewall)
firewall, err := firewall2.New(opts.PathNftables, logger, opts.ConfigFirewall, docker)
analyzerService := analyzer.New(opts.ConfigAnalyzer, logger, notifications)
return &daemon{
pidFile: pidFile,
socket: sock,
logger: logger,
firewall: firewall,
pidFile: pidFile,
socket: sock,
logger: logger,
firewall: firewall,
notifications: notifications,
analyzer: analyzerService,
docker: docker,
}, nil
}

View File

@@ -25,5 +25,8 @@
"daemon stopped": "Daemon stopped",
"daemon stop failed": "Daemon stop failed",
"daemon is not running": "Daemon is not running",
"daemon is not reopening logger": "The daemon did not reopen the log"
}
"daemon is not reopening logger": "The daemon did not reopen the log",
"alert.login.subject": "SSH login alert for user {{.User}} from {{.IP}}",
"alert.login.body": "Logged into the OS via ssh:\n Time: {{.Time}}\n IP: {{.IP}}\n User: {{.User}}\n Log: {{.Log}}"
}

View File

@@ -25,5 +25,8 @@
"daemon stopped": "Жын тоқтатылды",
"daemon stop failed": "Жынды тоқтату сәтсіз аяқталды",
"daemon is not running": "Демон жұмыс істемейді",
"daemon is not reopening logger": "Жын журналды қайта ашпады"
"daemon is not reopening logger": "Жын журналды қайта ашпады",
"alert.login.subject": "{{.IP}} IP мекенжайынан {{.User}} пайдаланушысына арналған SSH кіру хабарламасы",
"alert.login.body": "ОС-қа ssh арқылы кірді:\n Уақыт: {{.Time}}\n IP: {{.IP}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}"
}

View File

@@ -25,5 +25,8 @@
"daemon stopped": "Демон остановлен",
"daemon stop failed": "Остановка демона не удалась",
"daemon is not running": "Демон не запущен",
"daemon is not reopening logger": "Демон не открыл журнал повторно"
"daemon is not reopening logger": "Демон не открыл журнал повторно",
"alert.login.subject": "SSH-сообщение о входе пользователя {{.User}} с IP-адреса {{.IP}}",
"alert.login.body": "Вошли в ОС через ssh:\n Время: {{.Time}}\n IP: {{.IP}}\n Пользователь: {{.User}}\n Лог: {{.Log}}"
}

View File

@@ -12,6 +12,17 @@ const (
IPv6
)
func (v Version) ToNft() string {
switch v {
case IPv4:
return "ip"
case IPv6:
return "ip6"
default:
return "unknown"
}
}
func DetermineIPVersion(ip string) (ipNet string, version Version, err error) {
ipNet, version, err = parseCIDR(ip)
if err != nil {
@@ -21,6 +32,11 @@ func DetermineIPVersion(ip string) (ipNet string, version Version, err error) {
return
}
func IPVersion(ip string) (Version, error) {
_, version, err := parseIP(ip)
return version, err
}
func parseCIDR(parseIP string) (ipNet string, version Version, err error) {
_, parseIPNet, err := net.ParseCIDR(parseIP)
if err != nil {

View File

@@ -0,0 +1,45 @@
package analyzer
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
"github.com/spf13/viper"
)
type Setting struct {
Login Login
}
func InitSetting(path string) (Setting, error) {
if err := validate.IsTomlFile(path, "otherSettingsPath.analyzer"); err != nil {
return Setting{}, err
}
setting := settingDefault()
v := viper.New()
v.SetConfigType("toml")
v.SetConfigFile(path)
if err := v.ReadInConfig(); err != nil {
return Setting{}, err
}
if err := v.Unmarshal(&setting); err != nil {
return Setting{}, err
}
return setting, nil
}
func settingDefault() Setting {
return Setting{
Login: defaultLogin(),
}
}
func (s Setting) Validate() error {
if err := s.Login.Validate(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,21 @@
package analyzer
type Login struct {
Enabled bool `mapstructure:"enabled"`
Notify bool `mapstructure:"notify"`
SSHEnable bool `mapstructure:"ssh_enable"`
SSHNotify bool `mapstructure:"ssh_notify"`
}
func defaultLogin() Login {
return Login{
Enabled: true,
Notify: true,
SSHEnable: true,
SSHNotify: true,
}
}
func (l Login) Validate() error {
return nil
}

View File

@@ -1,11 +1,15 @@
package setting
type binaryLocations struct {
Nftables string `mapstructure:"nftables"`
Nftables string `mapstructure:"nftables"`
Journalctl string `mapstructure:"journalctl"`
Docker string `mapstructure:"docker"`
}
func binaryLocationsDefault() *binaryLocations {
return &binaryLocations{
Nftables: "/usr/sbin/nft",
Nftables: "/usr/sbin/nft",
Journalctl: "/bin/journalctl",
Docker: "/usr/bin/docker",
}
}

View File

@@ -4,24 +4,29 @@ import (
"errors"
"strings"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
)
type options struct {
ClearMode string `mapstructure:"clear_mode"`
SavesRules bool `mapstructure:"saves_rules"`
SavesRulesPath string `mapstructure:"saves_rules_path"`
DnsStrict bool `mapstructure:"dns_strict"`
DnsStrictNs bool `mapstructure:"dns_strict_ns"`
PacketFilter bool `mapstructure:"packet_filter"`
DockerSupport bool `mapstructure:"docker_support"`
}
func defaultOptions() options {
return options{
ClearMode: "global",
SavesRules: false,
SavesRulesPath: "/etc/nftables.conf",
DnsStrict: false,
DnsStrictNs: false,
PacketFilter: true,
DockerSupport: false,
}
}
@@ -45,3 +50,14 @@ func (o options) ValidateSavesRulesPath() error {
return nil
}
func (o options) ToClearMode() (firewall.ClearMode, error) {
switch o.ClearMode {
case "global":
return firewall.ClearModeGlobal, nil
case "own":
return firewall.ClearModeOwn, nil
}
return firewall.ClearModeGlobal, errors.New("invalid option clear_mode. Must be 'global' or 'own'")
}

View File

@@ -11,8 +11,11 @@ type policy struct {
DefaultAllowOutput bool `mapstructure:"default_allow_output"`
DefaultAllowForward bool `mapstructure:"default_allow_forward"`
InputDrop string `mapstructure:"input_drop"`
InputPriority int `mapstructure:"input_priority"`
OutputDrop string `mapstructure:"output_drop"`
OutputPriority int `mapstructure:"output_priority"`
ForwardDrop string `mapstructure:"forward_drop"`
ForwardPriority int `mapstructure:"forward_priority"`
}
func defaultPolicy() policy {
@@ -21,8 +24,11 @@ func defaultPolicy() policy {
DefaultAllowOutput: false,
DefaultAllowForward: false,
InputDrop: "drop",
InputPriority: -10,
OutputDrop: "reject",
OutputPriority: -10,
ForwardDrop: "drop",
ForwardPriority: -10,
}
}
@@ -47,8 +53,11 @@ func (p policy) ToConfigPolicy() (firewall.ConfigPolicy, error) {
DefaultAllowOutput: p.DefaultAllowOutput,
DefaultAllowForward: p.DefaultAllowForward,
InputDrop: inputDrop,
InputPriority: p.InputPriority,
OutputDrop: outputDrop,
OutputPriority: p.OutputPriority,
ForwardDrop: forwardDrop,
ForwardPriority: p.ForwardPriority,
}, nil
}
@@ -70,12 +79,23 @@ func (p policy) Validate() error {
if err := validateDrop(p.InputDrop, "input_drop"); err != nil {
return err
}
if err := validatePriority(p.InputPriority, "input_priority"); err != nil {
return err
}
if err := validateDrop(p.OutputDrop, "output_drop"); err != nil {
return err
}
if err := validatePriority(p.OutputPriority, "output_priority"); err != nil {
return err
}
if err := validateDrop(p.ForwardDrop, "forward_drop"); err != nil {
return err
}
if err := validatePriority(p.ForwardPriority, "forward_priority"); err != nil {
return err
}
return nil
}
@@ -86,3 +106,10 @@ func validateDrop(drop string, parameterName string) error {
}
return fmt.Errorf("invalid %s. Must be drop or reject", parameterName)
}
func validatePriority(priority int, parameterName string) error {
if priority < -50 || priority > 50 {
return fmt.Errorf("%s must be in range -50-50", parameterName)
}
return nil
}

View File

@@ -0,0 +1,129 @@
package notifications
import (
"errors"
"fmt"
"strings"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
"github.com/wneessen/go-mail"
)
type Email struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
AuthType string `mapstructure:"auth_type"`
TLSMode string `mapstructure:"tls_mode"`
TLSPolicy string `mapstructure:"tls_policy"`
TLSVerify bool `mapstructure:"tls_verify"`
From string `mapstructure:"from"`
To string `mapstructure:"to"`
}
func defaultEmail() Email {
return Email{
Host: "",
Port: 0,
Username: "",
Password: "",
AuthType: "PLAIN",
TLSMode: "STARTTLS",
TLSPolicy: "MANDATORY",
TLSVerify: true,
From: "",
To: "",
}
}
func (e Email) Validate() error {
if e.Host == "" {
return errors.New("host is not specified")
}
if e.Port == 0 {
return errors.New("port is not specified")
}
if err := validate.Port(e.Port, "port"); err != nil {
return err
}
if e.From == "" {
return errors.New("from is not specified")
}
if e.To == "" {
return errors.New("to is not specified")
}
if e.Username == "" {
return errors.New("username is not specified")
}
if e.Password == "" {
return errors.New("password is not specified")
}
return nil
}
func (e Email) ToTLSConfig() (notifications.TLS, error) {
mode, err := parseTLSMode(e.TLSMode)
if err != nil {
return notifications.TLS{}, err
}
policy, err := parseTLSPolicy(e.TLSPolicy)
if err != nil {
return notifications.TLS{}, err
}
return notifications.TLS{
Mode: mode,
Policy: policy,
Verify: e.TLSVerify,
}, nil
}
func ParseAuthType(authType string) (mail.SMTPAuthType, error) {
switch strings.ToUpper(authType) {
case "PLAIN":
return mail.SMTPAuthPlain, nil
case "LOGIN":
return mail.SMTPAuthLogin, nil
case "CRAM-MD5":
return mail.SMTPAuthCramMD5, nil
case "NONE":
return mail.SMTPAuthNoAuth, nil
}
return mail.SMTPAuthNoAuth, fmt.Errorf("unknown auth type: %s", authType)
}
func parseTLSMode(mode string) (notifications.TLSMode, error) {
switch strings.ToUpper(mode) {
case "STARTTLS":
return notifications.TLSModeStartTLS, nil
case "IMPLICIT":
return notifications.TLSModeImplicit, nil
case "NONE":
return notifications.TLSModeNone, nil
}
return notifications.TLSModeNone, fmt.Errorf("unknown tls mode: %s", mode)
}
func parseTLSPolicy(policy string) (notifications.TLSPolicy, error) {
switch strings.ToUpper(policy) {
case "MANDATORY":
return notifications.TLSPolicyMandatory, nil
case "OPPORTUNISTIC":
return notifications.TLSPolicyOpportunistic, nil
}
return notifications.TLSPolicyMandatory, fmt.Errorf("unknown tls policy: %s", policy)
}

View File

@@ -0,0 +1,67 @@
package notifications
import (
"errors"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
"github.com/spf13/viper"
)
type Setting struct {
Enabled bool `mapstructure:"enabled"`
ServerName string `mapstructure:"server_name"`
Email Email
}
func InitSetting(path string) (Setting, error) {
if err := validate.IsTomlFile(path, "otherSettingsPath.notifications"); err != nil {
return Setting{}, err
}
setting := settingDefault()
v := viper.New()
v.SetConfigType("toml")
v.SetConfigFile(path)
if err := v.ReadInConfig(); err != nil {
return Setting{}, err
}
if err := v.Unmarshal(&setting); err != nil {
return Setting{}, err
}
if !setting.Enabled {
return setting, nil
}
if err := setting.Validate(); err != nil {
return Setting{}, err
}
return setting, nil
}
func settingDefault() Setting {
return Setting{
Enabled: false,
ServerName: "server",
Email: defaultEmail(),
}
}
func (s Setting) Validate() error {
if s.ServerName == "" {
return errors.New("server_name is not specified")
}
if err := validate.NameWithDot(s.ServerName, "server_name"); err != nil {
return err
}
if err := s.Email.Validate(); err != nil {
return err
}
return nil
}

View File

@@ -1,17 +1,29 @@
package setting
import (
"errors"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
analyzerSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/analyzer"
firewallSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/firewall"
notificationsSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/notifications"
"github.com/wneessen/go-mail"
)
type otherSettingsPath struct {
Firewall string `mapstructure:"firewall"`
Firewall string `mapstructure:"firewall"`
Notifications string `mapstructure:"notifications"`
Analyzer string `mapstructure:"analyzer"`
}
func otherSettingsPathDefault() *otherSettingsPath {
return &otherSettingsPath{
Firewall: "/etc/kor-elf-shield/firewall.toml",
Firewall: "/etc/kor-elf-shield/firewall.toml",
Notifications: "/etc/kor-elf-shield/notifications.toml",
Analyzer: "/etc/kor-elf-shield/analyzer.toml",
}
}
@@ -36,6 +48,11 @@ func (o *otherSettingsPath) ToFirewallConfig() (firewall.Config, error) {
return firewall.Config{}, err
}
optionClearMode, err := setting.Options.ToClearMode()
if err != nil {
return firewall.Config{}, err
}
return firewall.Config{
InPorts: inPorts,
OutPorts: outPorts,
@@ -55,11 +72,13 @@ func (o *otherSettingsPath) ToFirewallConfig() (firewall.Config, error) {
OutIPs: IPs.OutIP6,
},
Options: firewall.ConfigOptions{
ClearMode: optionClearMode,
SavesRules: setting.Options.SavesRules,
SavesRulesPath: setting.Options.SavesRulesPath,
DnsStrict: setting.Options.DnsStrict,
DnsStrictNs: setting.Options.DnsStrictNs,
PacketFilter: setting.Options.PacketFilter,
DockerSupport: setting.Options.DockerSupport,
},
MetadataNaming: firewall.ConfigMetadata{
TableName: setting.MetadataNaming.TableName,
@@ -70,3 +89,74 @@ func (o *otherSettingsPath) ToFirewallConfig() (firewall.Config, error) {
Policy: configPolicy,
}, nil
}
func (o *otherSettingsPath) ToNotificationsConfig() (notifications.Config, error) {
setting, err := notificationsSetting.InitSetting(o.Notifications)
if err != nil {
return notifications.Config{}, err
}
authType := mail.SMTPAuthPlain
tls := notifications.TLS{}
if setting.Enabled {
authType, err = notificationsSetting.ParseAuthType(setting.Email.AuthType)
if err != nil {
return notifications.Config{}, err
}
tls, err = setting.Email.ToTLSConfig()
if err != nil {
return notifications.Config{}, err
}
}
return notifications.Config{
Enabled: setting.Enabled,
ServerName: setting.ServerName,
Email: notifications.Email{
Host: setting.Email.Host,
Port: uint(setting.Email.Port),
Username: setting.Email.Username,
Password: setting.Email.Password,
AuthType: authType,
TLS: tls,
From: setting.Email.From,
To: setting.Email.To,
},
}, nil
}
func (o *otherSettingsPath) ToAnalyzerConfig(binaryLocations *binaryLocations) (config.Config, error) {
if binaryLocations.Journalctl == "" {
return config.Config{}, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
"Parameter": "binaryLocations.journalctl",
}))
}
setting, err := analyzerSetting.InitSetting(o.Analyzer)
if err != nil {
return config.Config{}, err
}
if err := setting.Validate(); err != nil {
return config.Config{}, err
}
binPath := config.BinPath{
Journalctl: binaryLocations.Journalctl,
}
login := config.Login{
Enabled: setting.Login.Enabled,
Notify: setting.Login.Notify,
SSH: config.LoginSSH{
Enabled: setting.Login.SSHEnable,
Notify: setting.Login.SSHNotify,
},
}
return config.Config{
BinPath: binPath,
Login: login,
}, nil
}

View File

@@ -61,11 +61,17 @@ func (s setting) ToDaemonOptions() (daemon.DaemonOptions, error) {
return daemon.DaemonOptions{}, err
}
analyzerConfig, err := s.OtherSettingsPath.ToAnalyzerConfig(s.BinaryLocations)
if err != nil {
return daemon.DaemonOptions{}, err
}
return daemon.DaemonOptions{
PathPidFile: s.PidFile,
PathSocketFile: s.SocketFile,
PathNftables: s.BinaryLocations.Nftables,
ConfigFirewall: firewallConfig,
ConfigAnalyzer: analyzerConfig,
}, nil
}

View File

@@ -54,6 +54,17 @@ func Name(name string, parameterName string) error {
return nil
}
func NameWithDot(name string, parameterName string) error {
if name == "" {
return fmt.Errorf("%s is empty", parameterName)
}
re := regexp.MustCompile(`^[a-zA-Z0-9_\-\.]{1,64}$`)
if !re.MatchString(name) {
return fmt.Errorf("%s must not contain special characters", parameterName)
}
return nil
}
func Port(port int, parameterName string) error {
if port < 0 || port > 65535 {
return fmt.Errorf("%s must be in range 0-65535", parameterName)