Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbaf0304c3 | |||
|
1f8be77ab3
|
|||
|
d2795639da
|
|||
|
8638c49886
|
|||
|
66e6bad111
|
|||
|
1a6d6b813b
|
|||
|
9b8d07ccb3
|
|||
|
4b8622a870
|
|||
|
b9719f7eaf
|
|||
|
c424621615
|
|||
|
865f12d966
|
|||
|
b3a94855b8
|
|||
|
4d001a026c
|
|||
|
6e4bd17bfe
|
|||
|
0bcdb7bcc7
|
|||
|
5f2d5a1a9e
|
|||
|
542f7415b7
|
|||
|
8615c79f12
|
|||
|
b5686a2ee6
|
|||
|
e78685c130
|
|||
|
74dce294bf
|
|||
|
6929ac9bf5
|
|||
| 69157c90cb | |||
|
7054efd359
|
|||
|
57948fb639
|
|||
|
6e7b6093f1
|
41
CHANGELOG.md
41
CHANGELOG.md
@@ -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)
|
||||
***
|
||||
#### Русский
|
||||
|
||||
@@ -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.
|
||||
|
||||
15
README.md
15
README.md
@@ -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).
|
||||
* Уведомлять, если появится новый пользователь в системе.
|
||||
* Уведомлять, если изменились системные файлы.
|
||||
|
||||
@@ -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.
|
||||
|
||||
47
assets/configs/analyzer.toml
Normal file
47
assets/configs/analyzer.toml
Normal 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
170
assets/configs/notifications.toml
Normal file
170
assets/configs/notifications.toml
Normal 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 = ""
|
||||
@@ -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
3
go.mod
@@ -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
6
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
81
internal/daemon/analyzer/analyzer.go
Normal file
81
internal/daemon/analyzer/analyzer.go
Normal 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
|
||||
}
|
||||
5
internal/daemon/analyzer/config/bin.go
Normal file
5
internal/daemon/analyzer/config/bin.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package config
|
||||
|
||||
type BinPath struct {
|
||||
Journalctl string
|
||||
}
|
||||
6
internal/daemon/analyzer/config/config.go
Normal file
6
internal/daemon/analyzer/config/config.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
BinPath BinPath
|
||||
Login Login
|
||||
}
|
||||
12
internal/daemon/analyzer/config/login.go
Normal file
12
internal/daemon/analyzer/config/login.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package config
|
||||
|
||||
type Login struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
SSH LoginSSH
|
||||
}
|
||||
|
||||
type LoginSSH struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
}
|
||||
31
internal/daemon/analyzer/log/analysis.go
Normal file
31
internal/daemon/analyzer/log/analysis.go
Normal 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)
|
||||
}
|
||||
22
internal/daemon/analyzer/log/analysis/analysis.go
Normal file
22
internal/daemon/analyzer/log/analysis/analysis.go
Normal 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
|
||||
}
|
||||
87
internal/daemon/analyzer/log/analysis/ssh.go
Normal file
87
internal/daemon/analyzer/log/analysis/ssh.go
Normal 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
|
||||
}
|
||||
147
internal/daemon/analyzer/log/systemd.go
Normal file
147
internal/daemon/analyzer/log/systemd.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
79
internal/daemon/docker_monitor/chain/chains.go
Normal file
79
internal/daemon/docker_monitor/chain/chains.go
Normal 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()
|
||||
}
|
||||
32
internal/daemon/docker_monitor/chain/empty_chains.go
Normal file
32
internal/daemon/docker_monitor/chain/empty_chains.go
Normal 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{}
|
||||
}
|
||||
77
internal/daemon/docker_monitor/chain/new_chains.go
Normal file
77
internal/daemon/docker_monitor/chain/new_chains.go
Normal 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
|
||||
}
|
||||
66
internal/daemon/docker_monitor/client/bridge.go
Normal file
66
internal/daemon/docker_monitor/client/bridge.go
Normal 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
|
||||
}
|
||||
84
internal/daemon/docker_monitor/client/container.go
Normal file
84
internal/daemon/docker_monitor/client/container.go
Normal 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
|
||||
}
|
||||
158
internal/daemon/docker_monitor/client/docker.go
Normal file
158
internal/daemon/docker_monitor/client/docker.go
Normal 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
|
||||
}
|
||||
75
internal/daemon/docker_monitor/client/entity.go
Normal file
75
internal/daemon/docker_monitor/client/entity.go
Normal 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
|
||||
}
|
||||
188
internal/daemon/docker_monitor/docker.go
Normal file
188
internal/daemon/docker_monitor/docker.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
internal/daemon/docker_monitor/docker_not_support.go
Normal file
32
internal/daemon/docker_monitor/docker_not_support.go
Normal 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
|
||||
}
|
||||
26
internal/daemon/firewall/chain/chain.go
Normal file
26
internal/daemon/firewall/chain/chain.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: "",
|
||||
}
|
||||
|
||||
@@ -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: "",
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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: "",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
72
internal/daemon/firewall/docker.go
Normal file
72
internal/daemon/firewall/docker.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
43
internal/daemon/notifications/config.go
Normal file
43
internal/daemon/notifications/config.go
Normal 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"
|
||||
)
|
||||
151
internal/daemon/notifications/notifications.go
Normal file
151
internal/daemon/notifications/notifications.go
Normal 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 ¬ifications{
|
||||
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...)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
@@ -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}}"
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
45
internal/setting/analyzer/analyzer.go
Normal file
45
internal/setting/analyzer/analyzer.go
Normal 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
|
||||
}
|
||||
21
internal/setting/analyzer/login.go
Normal file
21
internal/setting/analyzer/login.go
Normal 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
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
129
internal/setting/notifications/email.go
Normal file
129
internal/setting/notifications/email.go
Normal 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)
|
||||
}
|
||||
67
internal/setting/notifications/notifications.go
Normal file
67
internal/setting/notifications/notifications.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user