Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a0cf7bd8a | |||
|
b938b73cfd
|
|||
|
ce031be060
|
|||
|
5e50bc179f
|
|||
|
279f58b644
|
|||
|
26365a519b
|
|||
|
d1f307d2ad
|
|||
|
ccf228242d
|
|||
|
5e12b1f6ab
|
|||
|
67abcc0ef2
|
|||
|
5ad40cdf9b
|
|||
|
374abcea80
|
|||
| 4748630b04 | |||
|
a75df70922
|
|||
|
a84f1ccde6
|
|||
|
0d13f851dd
|
|||
|
b04016c596
|
|||
|
8147e715f2
|
|||
|
f57172a2ea
|
|||
|
6c5a476d6e
|
|||
|
264f8ac60b
|
|||
|
b2a9f83a44
|
|||
|
6ac0a86d9d
|
|||
|
a6133c308e
|
|||
|
82b501d0ec
|
|||
|
ce6cbbe17e
|
|||
|
2de8aa29c4
|
|||
|
3afd4aa5f3
|
|||
|
42160ff5ab
|
|||
|
8798811806
|
|||
|
a10d56df79
|
|||
|
876592c38d
|
|||
|
e55660b098
|
|||
|
c6c3f991cc
|
|||
|
bc177f83b8
|
|||
|
48be913c57
|
|||
|
0a30733d27
|
|||
|
4a5492b1c5
|
|||
|
a3df113b07
|
|||
|
e034debeaa
|
|||
|
9134ab8ec0
|
|||
|
ba23474eab
|
|||
| 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
|
87
CHANGELOG.md
87
CHANGELOG.md
@@ -1,3 +1,90 @@
|
||||
## 0.5.0 (17.1.2026)
|
||||
***
|
||||
#### Русский
|
||||
* В настройках analyzer.toml добавил параметры local_enable и local_notify.
|
||||
* local_enable = Включает отслеживание локальных авторизаций (TTY, физический доступ). По умолчанию включён.
|
||||
* local_notify = Включает уведомления о локальных авторизациях. По умолчанию включён.
|
||||
* В настройках analyzer.toml добавил параметры su_enable и su_notify.
|
||||
* su_enable = Включает отслеживание авторизаций через su. По умолчанию включён.
|
||||
* su_notify = Включает уведомления об авторизациях через su. По умолчанию включён.
|
||||
* В настройках analyzer.toml добавил параметры sudo_enable и sudo_notify.
|
||||
* sudo_enable = Включает отслеживание авторизаций через sudo. По умолчанию выключен.
|
||||
* sudo_notify = Включает уведомления об авторизациях через sudo. По умолчанию включён.
|
||||
***
|
||||
#### English
|
||||
* Added local_enable and local_notify parameters to analyzer.toml settings.
|
||||
* local_enable = Enables tracking of local logins (TTY, physical access). Enabled by default.
|
||||
* local_notify = Enables notifications about local logins. Enabled by default.
|
||||
* Added su_enable and su_notify parameters to analyzer.toml settings.
|
||||
* su_enable = Enables tracking of logins via su. Enabled by default.
|
||||
* su_notify = Enables notifications about logins via su. Enabled by default.
|
||||
* Added sudo_enable and sudo_notify parameters to analyzer.toml settings.
|
||||
* sudo_enable = Enables tracking of logins via sudo. Off by default.
|
||||
* sudo_notify = Enables notifications about logins via sudo. Enabled by default.
|
||||
***
|
||||
## 0.4.0 (11.1.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Удалён параметр options.docker_support из файла firewall.toml. Настройки от Docker перенесены в файл docker.toml.
|
||||
* В настройках docker.toml добавил возможность переключать режим работы с Docker через параметр rule_strategy.
|
||||
* incremental = добавляются или удаляются только правила конкретного контейнера (сейчас по умолчанию)
|
||||
* rebuild = при любом изменении все цепочки Docker пересоздаются целиком (старый режим)
|
||||
* Исправлена ошибка:
|
||||
* Настройка binaryLocations.docker не работала.
|
||||
* Программа аварийно завершалась после остановки Docker'а.
|
||||
* Указанные в настройках IP-адреса не блокировались во время перенаправления в контейнер Docker.
|
||||
***
|
||||
#### English
|
||||
* Removed the options.docker_support parameter from firewall.toml. Docker settings have been moved to the docker.toml file.
|
||||
* Added the ability to switch Docker operation mode via the rule_strategy parameter to the docker.toml settings.
|
||||
* incremental = only rules for a specific container are added or removed (currently the default)
|
||||
* rebuild = any change rebuilds all Docker chains (old mode)
|
||||
* Fixed error:
|
||||
* The binaryLocations.docker setting did not work.
|
||||
* The program crashed after Docker was stopped.
|
||||
* The IP addresses specified in the settings were not blocked during redirection to the Docker container.
|
||||
***
|
||||
## 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.
|
||||
|
||||
110
assets/configs/analyzer.toml
Normal file
110
assets/configs/analyzer.toml
Normal file
@@ -0,0 +1,110 @@
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Отслеживать авторизаций
|
||||
# ***
|
||||
# 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
|
||||
|
||||
###
|
||||
# Включает отслеживание локальных авторизаций (TTY, физический доступ).
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables tracking of local authorizations (TTY, physical access).
|
||||
# Default: true
|
||||
###
|
||||
local_enable = true
|
||||
|
||||
###
|
||||
# Включает уведомления о локальных авторизациях.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables local authorization notifications.
|
||||
# Default: true
|
||||
###
|
||||
local_notify = true
|
||||
|
||||
###
|
||||
# Включает отслеживание, если кто-либо использует команду `su` для доступа к другой учетной записи.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables tracking if someone uses the `su` command to access another account.
|
||||
# Default: true
|
||||
###
|
||||
su_enable = true
|
||||
|
||||
###
|
||||
# Включает уведомления, если кто-либо использует команду `su` для доступа к другой учетной записи.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables notifications if someone uses the `su` command to access another account.
|
||||
# Default: true
|
||||
###
|
||||
su_notify = true
|
||||
|
||||
###
|
||||
# Включает отслеживание, если кто-либо использует команду `sudo` для доступа к другой учетной записи.
|
||||
#
|
||||
# ПРИМЕЧАНИЕ: Эта опция может стать обременительной, если команда sudo широко используется
|
||||
# для получения root-доступа администраторами или панелями управления.
|
||||
#
|
||||
# По умолчанию: false
|
||||
# ***
|
||||
# Enables tracking if someone uses the `sudo` command to access another account.
|
||||
#
|
||||
# NOTE: This option could become onerous if sudo is used extensively for root
|
||||
# access by administrators or control panels.
|
||||
#
|
||||
# Default: false
|
||||
###
|
||||
sudo_enable = false
|
||||
|
||||
###
|
||||
# Включает уведомления, если кто-либо использует команду `sudo` для доступа к другой учетной записи.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables notifications if someone uses the `sudo` command to access another account.
|
||||
# Default: true
|
||||
###
|
||||
sudo_notify = true
|
||||
|
||||
23
assets/configs/docker.toml
Normal file
23
assets/configs/docker.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
###
|
||||
# Включает поддержку docker.
|
||||
# По умолчанию: false
|
||||
# ***
|
||||
# Includes docker support.
|
||||
# Default: false
|
||||
###
|
||||
enabled = false
|
||||
|
||||
###
|
||||
# Стратегия управления правилами при запуске или остановке контейнеров в Docker:
|
||||
# rebuild = при любом изменении все цепочки Docker пересоздаются целиком
|
||||
# incremental = добавляются или удаляются только правила конкретного контейнера
|
||||
#
|
||||
# По умолчанию: "incremental"
|
||||
# ***
|
||||
# # Strategy for managing rules when container start or stop events occur in docker:
|
||||
# rebuild = any change causes all Docker chains to be rebuilt entirely
|
||||
# incremental = only rules for a specific container are added or removed
|
||||
#
|
||||
# Default: "incremental"
|
||||
###
|
||||
rule_strategy = "incremental"
|
||||
@@ -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 соответствует вашей ОС.
|
||||
@@ -409,6 +429,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 +461,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 +493,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 +548,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"
|
||||
@@ -34,7 +37,12 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
|
||||
_ = logger.Sync()
|
||||
}()
|
||||
|
||||
config, err := setting.Config.ToDaemonOptions()
|
||||
dockerService, dockerSupport, err := newDockerService(ctx, logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create docker service: %s", err))
|
||||
}
|
||||
|
||||
config, err := setting.Config.ToDaemonOptions(dockerSupport)
|
||||
if err != nil {
|
||||
logger.Fatal(err.Error())
|
||||
|
||||
@@ -43,7 +51,16 @@ 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
|
||||
}
|
||||
|
||||
d, err := daemon.NewDaemon(config, logger, notificationsService, dockerService)
|
||||
if err != nil {
|
||||
logger.Fatal(err.Error())
|
||||
|
||||
@@ -63,3 +80,31 @@ 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) (dockerService docker_monitor.Docker, dockerSupport bool, err error) {
|
||||
config, dockerSupport, err := setting.Config.OtherSettingsPath.ToDockerConfig(setting.Config.BinaryLocations)
|
||||
if err != nil {
|
||||
return docker_monitor.NewDockerNotSupport(), false, err
|
||||
}
|
||||
|
||||
if !dockerSupport {
|
||||
dockerService = docker_monitor.NewDockerNotSupport()
|
||||
return dockerService, false, nil
|
||||
}
|
||||
|
||||
dockerService, err = docker_monitor.New(&config, ctx, logger)
|
||||
if err != nil {
|
||||
return docker_monitor.NewDockerNotSupport(), false, err
|
||||
}
|
||||
|
||||
return dockerService, dockerSupport, nil
|
||||
}
|
||||
|
||||
115
internal/daemon/analyzer/analyzer.go
Normal file
115
internal/daemon/analyzer/analyzer.go
Normal file
@@ -0,0 +1,115 @@
|
||||
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
|
||||
|
||||
logChan chan analysisServices.Entry
|
||||
}
|
||||
|
||||
func New(config config2.Config, logger log.Logger, notify notifications.Notifications) Analyzer {
|
||||
var units []string
|
||||
|
||||
if config.Login.Enabled {
|
||||
if config.Login.SSH.Enabled {
|
||||
units = append(units, "_SYSTEMD_UNIT=ssh.service")
|
||||
}
|
||||
|
||||
if config.Login.Local.Enabled {
|
||||
units = append(units, "SYSLOG_IDENTIFIER=login")
|
||||
}
|
||||
|
||||
if config.Login.Su.Enabled {
|
||||
units = append(units, "SYSLOG_IDENTIFIER=su")
|
||||
}
|
||||
|
||||
if config.Login.Sudo.Enabled {
|
||||
units = append(units, "SYSLOG_IDENTIFIER=sudo")
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
logChan: make(chan analysisServices.Entry, 1000),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analyzer) Run(ctx context.Context) {
|
||||
go a.systemd.Run(ctx, a.logChan)
|
||||
go a.processLogs(ctx)
|
||||
a.logger.Debug("Analyzer is start")
|
||||
}
|
||||
|
||||
func (a *analyzer) processLogs(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case entry, ok := <-a.logChan:
|
||||
if !ok {
|
||||
// Channel closed
|
||||
return
|
||||
}
|
||||
a.logger.Debug(fmt.Sprintf("Received log entry: %s", entry))
|
||||
|
||||
switch {
|
||||
case entry.Unit == "ssh.service":
|
||||
if err := a.analysis.SSH(&entry); err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to analyze SSH logs: %s", err))
|
||||
}
|
||||
case entry.SyslogIdentifier == "login":
|
||||
if err := a.analysis.Locale(&entry); err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to analyze locale logs: %s", err))
|
||||
}
|
||||
case entry.SyslogIdentifier == "sudo":
|
||||
if err := a.analysis.Sudo(&entry); err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to analyze sudo logs: %s", err))
|
||||
}
|
||||
case entry.SyslogIdentifier == "su":
|
||||
if err := a.analysis.Su(&entry); err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to analyze su logs: %s", err))
|
||||
}
|
||||
default:
|
||||
a.logger.Debug(fmt.Sprintf("Unknown unit or SyslogIdentifier: %s", entry.Unit))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analyzer) Close() error {
|
||||
if err := a.systemd.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
close(a.logChan)
|
||||
|
||||
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
|
||||
}
|
||||
30
internal/daemon/analyzer/config/login.go
Normal file
30
internal/daemon/analyzer/config/login.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package config
|
||||
|
||||
type Login struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
SSH LoginSSH
|
||||
Local LoginLocal
|
||||
Su LoginSu
|
||||
Sudo LoginSudo
|
||||
}
|
||||
|
||||
type LoginSSH struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
}
|
||||
|
||||
type LoginLocal struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
}
|
||||
|
||||
type LoginSu struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
}
|
||||
|
||||
type LoginSudo struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
}
|
||||
52
internal/daemon/analyzer/log/analysis.go
Normal file
52
internal/daemon/analyzer/log/analysis.go
Normal file
@@ -0,0 +1,52 @@
|
||||
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
|
||||
Locale(entry *analysisServices.Entry) error
|
||||
Su(entry *analysisServices.Entry) error
|
||||
Sudo(entry *analysisServices.Entry) error
|
||||
}
|
||||
|
||||
type analysis struct {
|
||||
sshService analysisServices.Analysis
|
||||
localeService analysisServices.Analysis
|
||||
suService analysisServices.Analysis
|
||||
sudoService 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),
|
||||
localeService: analysisServices.NewLocale(config, logger, notify),
|
||||
suService: analysisServices.NewSu(config, logger, notify),
|
||||
sudoService: analysisServices.NewSudo(config, logger, notify),
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analysis) SSH(entry *analysisServices.Entry) error {
|
||||
return a.sshService.Process(entry)
|
||||
}
|
||||
|
||||
func (a *analysis) Locale(entry *analysisServices.Entry) error {
|
||||
return a.localeService.Process(entry)
|
||||
}
|
||||
|
||||
func (a *analysis) Su(entry *analysisServices.Entry) error {
|
||||
return a.suService.Process(entry)
|
||||
}
|
||||
|
||||
func (a *analysis) Sudo(entry *analysisServices.Entry) error {
|
||||
return a.sudoService.Process(entry)
|
||||
}
|
||||
29
internal/daemon/analyzer/log/analysis/analysis.go
Normal file
29
internal/daemon/analyzer/log/analysis/analysis.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Analysis interface {
|
||||
Process(entry *Entry) error
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
Message string
|
||||
Unit string
|
||||
PID string
|
||||
SyslogIdentifier string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
type processReturn struct {
|
||||
found bool
|
||||
subject string
|
||||
body string
|
||||
}
|
||||
|
||||
type EmptyAnalysis struct{}
|
||||
|
||||
func (empty *EmptyAnalysis) Process(_ *Entry) error {
|
||||
return nil
|
||||
}
|
||||
78
internal/daemon/analyzer/log/analysis/locale.go
Normal file
78
internal/daemon/analyzer/log/analysis/locale.go
Normal file
@@ -0,0 +1,78 @@
|
||||
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 locale struct {
|
||||
login localeLogin
|
||||
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
}
|
||||
|
||||
type localeLogin struct {
|
||||
enabled bool
|
||||
notify bool
|
||||
}
|
||||
|
||||
func NewLocale(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
||||
if !config.Login.Enabled || !config.Login.Local.Enabled {
|
||||
return &EmptyAnalysis{}
|
||||
}
|
||||
|
||||
return &locale{
|
||||
login: localeLogin{
|
||||
enabled: config.Login.Enabled && config.Login.SSH.Enabled,
|
||||
notify: config.Login.Notify && config.Login.SSH.Notify,
|
||||
},
|
||||
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *locale) Process(entry *Entry) error {
|
||||
if l.login.enabled {
|
||||
result, err := l.login.process(entry)
|
||||
if err != nil {
|
||||
l.logger.Error(fmt.Sprintf("Failed to process TTY login: %s", err))
|
||||
} else if result.found {
|
||||
if l.login.notify {
|
||||
l.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
|
||||
}
|
||||
l.logger.Info(fmt.Sprintf("TTY login detected: %s", entry.Message))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *localeLogin) process(entry *Entry) (processReturn, error) {
|
||||
re := regexp.MustCompile(`^pam_unix\(login:session\): session opened for user (\S+)\(\S+\) by \S+`)
|
||||
matches := re.FindStringSubmatch(entry.Message)
|
||||
|
||||
if matches != nil {
|
||||
user := matches[1]
|
||||
|
||||
return processReturn{
|
||||
found: true,
|
||||
subject: i18n.Lang.T("alert.login.locale.subject", map[string]any{
|
||||
"User": user,
|
||||
}),
|
||||
body: i18n.Lang.T("alert.login.locale.body", map[string]any{
|
||||
"User": user,
|
||||
"Log": entry.Message,
|
||||
"Time": entry.Time,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return processReturn{found: false}, nil
|
||||
}
|
||||
81
internal/daemon/analyzer/log/analysis/ssh.go
Normal file
81
internal/daemon/analyzer/log/analysis/ssh.go
Normal file
@@ -0,0 +1,81 @@
|
||||
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
|
||||
}
|
||||
|
||||
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) (processReturn, 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 processReturn{
|
||||
found: true,
|
||||
subject: i18n.Lang.T("alert.login.ssh.subject", map[string]any{
|
||||
"User": user,
|
||||
"IP": ip,
|
||||
}),
|
||||
body: i18n.Lang.T("alert.login.ssh.body", map[string]any{
|
||||
"User": user,
|
||||
"IP": ip,
|
||||
"Log": entry.Message,
|
||||
"Time": entry.Time,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return processReturn{found: false}, nil
|
||||
}
|
||||
81
internal/daemon/analyzer/log/analysis/su.go
Normal file
81
internal/daemon/analyzer/log/analysis/su.go
Normal file
@@ -0,0 +1,81 @@
|
||||
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 su struct {
|
||||
login suLogin
|
||||
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
}
|
||||
|
||||
type suLogin struct {
|
||||
enabled bool
|
||||
notify bool
|
||||
}
|
||||
|
||||
func NewSu(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
||||
if !config.Login.Enabled || !config.Login.Su.Enabled {
|
||||
return &EmptyAnalysis{}
|
||||
}
|
||||
|
||||
return &su{
|
||||
login: suLogin{
|
||||
enabled: config.Login.Enabled && config.Login.Su.Enabled,
|
||||
notify: config.Login.Notify && config.Login.Su.Notify,
|
||||
},
|
||||
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *su) Process(entry *Entry) error {
|
||||
if l.login.enabled {
|
||||
result, err := l.login.process(entry)
|
||||
if err != nil {
|
||||
l.logger.Error(fmt.Sprintf("Failed to process Su login: %s", err))
|
||||
} else if result.found {
|
||||
if l.login.notify {
|
||||
l.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
|
||||
}
|
||||
l.logger.Info(fmt.Sprintf("Su login detected: %s", entry.Message))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *suLogin) process(entry *Entry) (processReturn, error) {
|
||||
re := regexp.MustCompile(`^pam_unix\(su:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`)
|
||||
matches := re.FindStringSubmatch(entry.Message)
|
||||
|
||||
if matches != nil {
|
||||
user := matches[1]
|
||||
byUser := matches[2]
|
||||
|
||||
return processReturn{
|
||||
found: true,
|
||||
subject: i18n.Lang.T("alert.login.su.subject", map[string]any{
|
||||
"User": user,
|
||||
"ByUser": byUser,
|
||||
}),
|
||||
body: i18n.Lang.T("alert.login.su.body", map[string]any{
|
||||
"User": user,
|
||||
"ByUser": byUser,
|
||||
"Log": entry.Message,
|
||||
"Time": entry.Time,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return processReturn{found: false}, nil
|
||||
}
|
||||
81
internal/daemon/analyzer/log/analysis/sudo.go
Normal file
81
internal/daemon/analyzer/log/analysis/sudo.go
Normal file
@@ -0,0 +1,81 @@
|
||||
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 sudo struct {
|
||||
login sudoLogin
|
||||
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
}
|
||||
|
||||
type sudoLogin struct {
|
||||
enabled bool
|
||||
notify bool
|
||||
}
|
||||
|
||||
func NewSudo(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
||||
if !config.Login.Enabled || !config.Login.Su.Enabled {
|
||||
return &EmptyAnalysis{}
|
||||
}
|
||||
|
||||
return &sudo{
|
||||
login: sudoLogin{
|
||||
enabled: config.Login.Enabled && config.Login.Sudo.Enabled,
|
||||
notify: config.Login.Notify && config.Login.Sudo.Notify,
|
||||
},
|
||||
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sudo) 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 Sudo 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("Sudo login detected: %s", entry.Message))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sudoLogin) process(entry *Entry) (processReturn, error) {
|
||||
re := regexp.MustCompile(`^pam_unix\(sudo:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`)
|
||||
matches := re.FindStringSubmatch(entry.Message)
|
||||
|
||||
if matches != nil {
|
||||
user := matches[1]
|
||||
byUser := matches[2]
|
||||
|
||||
return processReturn{
|
||||
found: true,
|
||||
subject: i18n.Lang.T("alert.login.sudo.subject", map[string]any{
|
||||
"User": user,
|
||||
"ByUser": byUser,
|
||||
}),
|
||||
body: i18n.Lang.T("alert.login.sudo.body", map[string]any{
|
||||
"User": user,
|
||||
"ByUser": byUser,
|
||||
"Log": entry.Message,
|
||||
"Time": entry.Time,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return processReturn{found: false}, nil
|
||||
}
|
||||
151
internal/daemon/analyzer/log/systemd.go
Normal file
151
internal/daemon/analyzer/log/systemd.go
Normal file
@@ -0,0 +1,151 @@
|
||||
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"`
|
||||
SyslogIdentifier string `json:"SYSLOG_IDENTIFIER"`
|
||||
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
|
||||
}
|
||||
|
||||
s.logger.Debug("Journalctl started")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
if err := s.watch(ctx, 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, logChan chan<- analysisServices.Entry) error {
|
||||
args := []string{"-f", "-n", "0", "-o", "json"}
|
||||
for index, unit := range s.units {
|
||||
if index > 0 {
|
||||
args = append(args, "+")
|
||||
}
|
||||
args = append(args, unit)
|
||||
}
|
||||
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,
|
||||
SyslogIdentifier: raw.SyslogIdentifier,
|
||||
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)
|
||||
|
||||
|
||||
87
internal/daemon/docker_monitor/chain/chains.go
Normal file
87
internal/daemon/docker_monitor/chain/chains.go
Normal file
@@ -0,0 +1,87 @@
|
||||
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, comment string) error {
|
||||
args := []string{rule, "jump", d.name, comment}
|
||||
return data.AddRule(args...)
|
||||
}
|
||||
|
||||
func (d *Data) AddRule(rule ...string) error {
|
||||
return d.chain.AddRule(rule...)
|
||||
}
|
||||
|
||||
func (d *Data) RemoveRuleByHandle(handle uint64) error {
|
||||
return d.chain.RemoveRuleByHandle(handle)
|
||||
}
|
||||
|
||||
func (d *Data) ListRules() ([]nftChain.Rule, error) {
|
||||
return d.chain.ListRules()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
42
internal/daemon/docker_monitor/client/bridge.go
Normal file
42
internal/daemon/docker_monitor/client/bridge.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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) bridgeInfo(bridgeID string) (DockerBridgeInspect, error) {
|
||||
args := []string{"network", "inspect", bridgeID}
|
||||
result, err := d.command(args...)
|
||||
if err != nil {
|
||||
return DockerBridgeInspect{}, fmt.Errorf("failed to get bridge name: %s", err.Error())
|
||||
}
|
||||
|
||||
var info []DockerBridgeInspect
|
||||
if err := json.Unmarshal(result, &info); err != nil {
|
||||
return DockerBridgeInspect{}, err
|
||||
}
|
||||
|
||||
return info[0], 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", "--no-trunc", "--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
|
||||
}
|
||||
262
internal/daemon/docker_monitor/client/docker.go
Normal file
262
internal/daemon/docker_monitor/client/docker.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type Docker interface {
|
||||
FetchBridges() (Bridges, error)
|
||||
FetchBridge(bridgeID string) (Bridge, error)
|
||||
FetchContainers(bridgeID string) (Containers, error)
|
||||
FetchContainer(containerID string) (Container, error)
|
||||
|
||||
Events() <-chan Event
|
||||
EventsClose() error
|
||||
}
|
||||
|
||||
type docker struct {
|
||||
path string
|
||||
ctx context.Context
|
||||
logger log.Logger
|
||||
|
||||
cmd *exec.Cmd
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
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 {
|
||||
bridge, err := d.FetchBridge(bridgeId)
|
||||
if err != nil {
|
||||
d.logger.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
bridges = append(bridges, bridge)
|
||||
}
|
||||
|
||||
return bridges, nil
|
||||
}
|
||||
|
||||
func (d *docker) FetchBridge(bridgeID string) (Bridge, error) {
|
||||
|
||||
bridgeInfo, err := d.bridgeInfo(bridgeID)
|
||||
if err != nil {
|
||||
return Bridge{}, err
|
||||
}
|
||||
|
||||
var containers Containers
|
||||
containers, err = d.FetchContainers(bridgeID)
|
||||
if err != nil {
|
||||
d.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
bridgeName := bridgeInfo.Options.Name
|
||||
if bridgeName == "" {
|
||||
bridgeName = bridgeNameFromID(bridgeID)
|
||||
}
|
||||
|
||||
var bridgeSubnet []string
|
||||
if bridgeInfo.IPAM.Config != nil {
|
||||
for _, config := range bridgeInfo.IPAM.Config {
|
||||
bridgeSubnet = append(bridgeSubnet, config.Subnet)
|
||||
}
|
||||
}
|
||||
|
||||
return Bridge{
|
||||
ID: bridgeInfo.ID,
|
||||
Name: bridgeName,
|
||||
Subnets: bridgeSubnet,
|
||||
Containers: containers,
|
||||
}, 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 {
|
||||
container, err := d.FetchContainer(containerID)
|
||||
if err != nil {
|
||||
d.logger.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
containers = append(containers, container)
|
||||
}
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (d *docker) FetchContainer(containerID string) (Container, error) {
|
||||
info, err := d.containerNetworks(containerID)
|
||||
if err != nil {
|
||||
return Container{}, err
|
||||
}
|
||||
|
||||
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,
|
||||
NetworkID: networkData.NetworkID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Container{
|
||||
ID: containerID,
|
||||
Networks: networks,
|
||||
}, 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 Event {
|
||||
eventsChan := make(chan Event)
|
||||
|
||||
d.logger.Debug("Starting docker monitor")
|
||||
go func() {
|
||||
defer close(eventsChan)
|
||||
for {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return
|
||||
default:
|
||||
if err := d.watch(eventsChan); err != nil {
|
||||
d.logger.Error(fmt.Sprintf("Docker monitor exited with error: %v", err))
|
||||
}
|
||||
|
||||
// Pause before restarting to avoid CPU load during persistent errors
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return
|
||||
case <-time.After(15 * time.Second):
|
||||
d.logger.Warn("Docker connection lost. Restarting in 15s...")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return eventsChan
|
||||
}
|
||||
|
||||
func (d *docker) watch(eventsChan chan Event) error {
|
||||
args := []string{
|
||||
"events",
|
||||
|
||||
"--filter", "type=container",
|
||||
"--filter", "event=start",
|
||||
"--filter", "event=die",
|
||||
|
||||
"--filter", "type=network",
|
||||
"--filter", "event=create",
|
||||
"--filter", "event=destroy",
|
||||
|
||||
"--format",
|
||||
"{{json .}}",
|
||||
}
|
||||
cmd := exec.CommandContext(d.ctx, d.path, args...)
|
||||
d.mu.Lock()
|
||||
d.cmd = cmd
|
||||
d.mu.Unlock()
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if scanner.Text() == "" {
|
||||
return fmt.Errorf("empty line")
|
||||
}
|
||||
|
||||
var dockerEvent DockerEvent
|
||||
if err := json.Unmarshal([]byte(scanner.Text()), &dockerEvent); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal docker event: %v", err)
|
||||
}
|
||||
|
||||
if dockerEvent.Type == "" || dockerEvent.Action == "" || dockerEvent.Actor.ID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
eventsChan <- Event{
|
||||
Type: dockerEvent.Type,
|
||||
Action: dockerEvent.Action,
|
||||
ID: dockerEvent.Actor.ID,
|
||||
Message: scanner.Text(),
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func (d *docker) EventsClose() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.cmd != nil && d.cmd.Process != nil {
|
||||
d.logger.Debug("Stopping docker monitor")
|
||||
|
||||
// Force docker monitor to quit on shutdown
|
||||
return d.cmd.Process.Kill()
|
||||
}
|
||||
|
||||
d.logger.Debug("Docker monitor stopped")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bridgeNameFromID(bridgeID string) string {
|
||||
if len(bridgeID) > 12 {
|
||||
bridgeID = bridgeID[:12]
|
||||
}
|
||||
return fmt.Sprintf("br-%s", bridgeID)
|
||||
}
|
||||
104
internal/daemon/docker_monitor/client/entity.go
Normal file
104
internal/daemon/docker_monitor/client/entity.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Type string
|
||||
Action string
|
||||
ID string // Full 64-char ID (Actor.ID)
|
||||
Message string // debug
|
||||
}
|
||||
|
||||
type DockerEvent struct {
|
||||
Type string `json:"Type"` // container, network
|
||||
Action string `json:"Action"` // start, die, create, destroy
|
||||
Actor struct {
|
||||
ID string `json:"ID"`
|
||||
} `json:"Actor"`
|
||||
}
|
||||
|
||||
type Bridges []Bridge
|
||||
|
||||
type Bridge struct {
|
||||
ID string
|
||||
Name string
|
||||
Subnets []string
|
||||
Containers Containers
|
||||
}
|
||||
|
||||
type DockerBridgeInspect struct {
|
||||
ID string `json:"Id"`
|
||||
Options struct {
|
||||
Name string `json:"com.docker.network.bridge.name"`
|
||||
} `json:"Options"`
|
||||
IPAM struct {
|
||||
Config []struct {
|
||||
Subnet string `json:"Subnet"`
|
||||
} `json:"Config"`
|
||||
} `json:"IPAM"`
|
||||
}
|
||||
|
||||
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"
|
||||
NetworkID string
|
||||
}
|
||||
|
||||
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"`
|
||||
NetworkID string `json:"NetworkID"`
|
||||
} `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
|
||||
}
|
||||
13
internal/daemon/docker_monitor/config.go
Normal file
13
internal/daemon/docker_monitor/config.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package docker_monitor
|
||||
|
||||
type Config struct {
|
||||
Path string
|
||||
RuleStrategy RuleStrategy
|
||||
}
|
||||
|
||||
type RuleStrategy int8
|
||||
|
||||
const (
|
||||
RuleStrategyRebuild RuleStrategy = iota + 1
|
||||
RuleStrategyIncremental
|
||||
)
|
||||
74
internal/daemon/docker_monitor/docker.go
Normal file
74
internal/daemon/docker_monitor/docker.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package docker_monitor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/rule_strategy"
|
||||
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 {
|
||||
dockerClient client.Docker
|
||||
ruleStrategy rule_strategy.Strategy
|
||||
logger log.Logger
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func New(config *Config, ctx context.Context, logger log.Logger) (Docker, error) {
|
||||
dockerClient := client.NewDocker(config.Path, ctx, logger)
|
||||
ruleStrategy, err := newRuleStrategy(config, dockerClient, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &docker{
|
||||
dockerClient: dockerClient,
|
||||
logger: logger,
|
||||
ctx: ctx,
|
||||
ruleStrategy: ruleStrategy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *docker) NftReload(newNoneChain func(chain string) (nftChain.Chain, error)) error {
|
||||
return d.ruleStrategy.Reload(newNoneChain)
|
||||
}
|
||||
|
||||
func (d *docker) NftChains() chain.Chains {
|
||||
return d.ruleStrategy.Chains()
|
||||
}
|
||||
|
||||
func (d *docker) Run() {
|
||||
events := d.dockerClient.Events()
|
||||
for {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return
|
||||
case event := <-events:
|
||||
if event.Message == "" {
|
||||
continue
|
||||
}
|
||||
d.logger.Debug("Docker event received: " + event.Message)
|
||||
d.ruleStrategy.Event(&event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *docker) Close() error {
|
||||
return d.dockerClient.EventsClose()
|
||||
}
|
||||
|
||||
func (d *docker) chainCommand(chainData chain.Data, rule string) {
|
||||
if err := chainData.AddRule(rule); err != nil {
|
||||
d.logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
22
internal/daemon/docker_monitor/rule_strategy.go
Normal file
22
internal/daemon/docker_monitor/rule_strategy.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package docker_monitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/client"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/rule_strategy"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
func newRuleStrategy(config *Config, dockerClient client.Docker, logger log.Logger) (rule_strategy.Strategy, error) {
|
||||
generate := rule_strategy.NewGenerator(dockerClient, logger)
|
||||
|
||||
switch config.RuleStrategy {
|
||||
case RuleStrategyRebuild:
|
||||
return rule_strategy.NewRebuildStrategy(generate), nil
|
||||
case RuleStrategyIncremental:
|
||||
return rule_strategy.NewIncrementalStrategy(generate, dockerClient, logger), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid option rule_strategy")
|
||||
}
|
||||
174
internal/daemon/docker_monitor/rule_strategy/generator.go
Normal file
174
internal/daemon/docker_monitor/rule_strategy/generator.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package rule_strategy
|
||||
|
||||
import (
|
||||
"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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type Generator interface {
|
||||
GenerateAll(chains chain.Chains, isComment bool)
|
||||
GenerateBridge(bridge client.Bridge, chain chain.Chains, isComment bool)
|
||||
GenerateContainer(container client.Container, bridgeName string, chain chain.Chains, isComment bool)
|
||||
ClearChains(chains chain.Chains)
|
||||
AddRule(chainData chain.Data, rule string)
|
||||
}
|
||||
|
||||
type generator struct {
|
||||
dockerClient client.Docker
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func NewGenerator(dockerClient client.Docker, logger log.Logger) Generator {
|
||||
return &generator{
|
||||
dockerClient: dockerClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) GenerateAll(chains chain.Chains, isComment bool) {
|
||||
listChains := chains.List()
|
||||
|
||||
if err := listChains.ForwardCT.JumpTo(&listChains.ForwardFilter, "", ""); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.ForwardBridge.JumpTo(&listChains.ForwardFilter, "", ""); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.DockerFilterFirst.JumpTo(&listChains.DockerFilter, "", ""); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.DockerFilterSecond.JumpTo(&listChains.DockerFilter, "", ""); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
bridges, err := g.dockerClient.FetchBridges()
|
||||
if err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, bridge := range bridges {
|
||||
g.GenerateBridge(bridge, chains, isComment)
|
||||
|
||||
if bridge.Containers == nil {
|
||||
continue
|
||||
}
|
||||
for _, container := range bridge.Containers {
|
||||
g.GenerateContainer(container, bridge.Name, chains, isComment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) GenerateBridge(bridge client.Bridge, chain chain.Chains, isComment bool) {
|
||||
listChains := chain.List()
|
||||
|
||||
var rule string
|
||||
comment := ""
|
||||
if isComment {
|
||||
comment = fmt.Sprintf("comment \"bridge_id:%s\"", bridge.ID)
|
||||
}
|
||||
|
||||
rule = fmt.Sprintf("iifname != \"%s\" oifname \"%s\" counter drop %s", bridge.Name, bridge.Name, comment)
|
||||
g.AddRule(listChains.DockerFilterSecond, rule)
|
||||
|
||||
rule = fmt.Sprintf("iifname \"%s\" counter accept %s", bridge.Name, comment)
|
||||
g.AddRule(listChains.ForwardFilter, rule)
|
||||
|
||||
rule = fmt.Sprintf("oifname \"%s\" counter", bridge.Name)
|
||||
if err := listChains.DockerFilter.JumpTo(&listChains.ForwardBridge, rule, comment); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
rule = fmt.Sprintf("oifname \"%s\" ct state related,established counter accept %s", bridge.Name, comment)
|
||||
g.AddRule(listChains.ForwardCT, rule)
|
||||
|
||||
for _, subnet := range bridge.Subnets {
|
||||
rule = fmt.Sprintf("ip saddr %s oifname != \"%s\" counter masquerade %s", subnet, bridge.Name, comment)
|
||||
g.AddRule(listChains.PostroutingNat, rule)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) GenerateContainer(container client.Container, bridgeName string, chain chain.Chains, isComment bool) {
|
||||
listChains := chain.List()
|
||||
var rule string
|
||||
comment := ""
|
||||
if isComment {
|
||||
comment = fmt.Sprintf("comment \"container_id:%s\"", container.ID)
|
||||
}
|
||||
|
||||
for _, ipInfo := range container.Networks.IPAddresses {
|
||||
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" counter drop %s", ipInfo.NftPrefix(), ipInfo.Address, bridgeName, comment)
|
||||
g.AddRule(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 %s", hostInfo.IP.NftPrefix(), hostInfo.IP.Address, port.Protocol, hostInfo.Port, comment)
|
||||
g.AddRule(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 %s", bridgeName, port.Protocol, hostInfo.Port, ipInfo.NftPrefix(), ipInfo.Address, port.Port, comment)
|
||||
g.AddRule(listChains.DockerNat, rule)
|
||||
|
||||
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" oifname \"%s\" %s dport %s counter accept %s", ipInfo.NftPrefix(), ipInfo.Address, bridgeName, bridgeName, port.Protocol, port.Port, comment)
|
||||
g.AddRule(listChains.DockerFilterFirst, rule)
|
||||
continue
|
||||
}
|
||||
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" oifname \"%s\" %s dport %s counter accept %s", ipInfo.NftPrefix(), ipInfo.Address, bridgeName, bridgeName, port.Protocol, port.Port, comment)
|
||||
g.AddRule(listChains.DockerFilterFirst, rule)
|
||||
|
||||
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" %s dport %s counter dnat to %s:%s %s", hostInfo.IP.NftPrefix(), hostInfo.IP.Address, bridgeName, port.Protocol, hostInfo.Port, ipInfo.Address, port.Port, comment)
|
||||
g.AddRule(listChains.DockerNat, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) ClearChains(chains chain.Chains) {
|
||||
listChains := chains.List()
|
||||
|
||||
if err := listChains.DockerNat.Clear(); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.PostroutingNat.Clear(); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.PreroutingFilter.Clear(); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.DockerFilter.Clear(); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.DockerFilterFirst.Clear(); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.DockerFilterSecond.Clear(); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.ForwardFilter.Clear(); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := listChains.ForwardBridge.Clear(); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := listChains.ForwardCT.Clear(); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) AddRule(chainData chain.Data, rule string) {
|
||||
if err := chainData.AddRule(rule); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package rule_strategy
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
type Strategy interface {
|
||||
Reload(newNoneChain func(chain string) (nftChain.Chain, error)) error
|
||||
Chains() chain.Chains
|
||||
Event(event *client.Event)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package rule_strategy
|
||||
|
||||
import (
|
||||
"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 incrementalStrategy struct {
|
||||
dockerClient client.Docker
|
||||
chains chain.Chains
|
||||
generator Generator
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func NewIncrementalStrategy(generator Generator, dockerClient client.Docker, logger log.Logger) Strategy {
|
||||
return &incrementalStrategy{
|
||||
dockerClient: dockerClient,
|
||||
generator: generator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) Reload(newNoneChain func(chain string) (nftChain.Chain, error)) error {
|
||||
chains, err := chain.NewChains(newNoneChain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.chains = chains
|
||||
|
||||
i.generator.GenerateAll(i.chains, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) Chains() chain.Chains {
|
||||
return i.chains
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) Event(event *client.Event) {
|
||||
if event == nil || event.ID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if event.Type == "container" {
|
||||
if event.Action == "start" {
|
||||
if err := i.eventContainerStart(event.ID); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to handle container start event: %s", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if event.Action == "die" {
|
||||
i.eventContainerStop(event.ID)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if event.Type == "network" {
|
||||
if event.Action == "create" {
|
||||
if err := i.eventNetworkCreate(event.ID); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to handle network create event: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if event.Action == "destroy" {
|
||||
i.eventNetworkDestroy(event.ID)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) eventContainerStart(containerId string) error {
|
||||
container, err := i.dockerClient.FetchContainer(containerId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ipInfo := range container.Networks.IPAddresses {
|
||||
bridge, err := i.dockerClient.FetchBridge(ipInfo.NetworkID)
|
||||
if err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to fetch bridge for container %s: %s", containerId, err))
|
||||
continue
|
||||
}
|
||||
i.generator.GenerateContainer(container, bridge.Name, i.chains, true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) eventContainerStop(containerId string) {
|
||||
listChains := i.chains.List()
|
||||
|
||||
if err := i.nftRuleDeleteContainer(containerId, &listChains.PreroutingFilter); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete container %s rules: %s", containerId, err))
|
||||
}
|
||||
|
||||
if err := i.nftRuleDeleteContainer(containerId, &listChains.DockerNat); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete container %s rules: %s", containerId, err))
|
||||
}
|
||||
|
||||
if err := i.nftRuleDeleteContainer(containerId, &listChains.DockerFilterFirst); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete container %s rules: %s", containerId, err))
|
||||
}
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) nftRuleDeleteContainer(containerId string, chain *chain.Data) error {
|
||||
rules, err := chain.ListRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.Comment != "container_id:"+containerId {
|
||||
continue
|
||||
}
|
||||
if err := chain.RemoveRuleByHandle(rule.Handle); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete container %s rule: %s", containerId, err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) eventNetworkCreate(bridgeId string) error {
|
||||
bridge, err := i.dockerClient.FetchBridge(bridgeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.generator.GenerateBridge(bridge, i.chains, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) eventNetworkDestroy(bridgeId string) {
|
||||
listChains := i.chains.List()
|
||||
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.DockerFilterSecond); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
|
||||
}
|
||||
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.ForwardFilter); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
|
||||
}
|
||||
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.ForwardBridge); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
|
||||
}
|
||||
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.ForwardCT); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
|
||||
}
|
||||
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.PostroutingNat); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
|
||||
}
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) nftRuleDeleteBridge(bridgeId string, chain *chain.Data) error {
|
||||
rules, err := chain.ListRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.Comment != "bridge_id:"+bridgeId {
|
||||
continue
|
||||
}
|
||||
if err := chain.RemoveRuleByHandle(rule.Handle); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rule: %s", bridgeId, err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package rule_strategy
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
type rebuildStrategy struct {
|
||||
chains chain.Chains
|
||||
generator Generator
|
||||
}
|
||||
|
||||
func NewRebuildStrategy(generator Generator) Strategy {
|
||||
return &rebuildStrategy{
|
||||
generator: generator,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rebuildStrategy) Reload(newNoneChain func(chain string) (nftChain.Chain, error)) error {
|
||||
chains, err := chain.NewChains(newNoneChain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.chains = chains
|
||||
|
||||
r.generator.GenerateAll(r.chains, false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rebuildStrategy) Chains() chain.Chains {
|
||||
return r.chains
|
||||
}
|
||||
|
||||
func (r *rebuildStrategy) Event(event *client.Event) {
|
||||
if event == nil || event.Type != "container" {
|
||||
return
|
||||
}
|
||||
|
||||
r.generator.ClearChains(r.chains)
|
||||
r.generator.GenerateAll(r.chains, false)
|
||||
}
|
||||
67
internal/daemon/firewall/chain/chain.go
Normal file
67
internal/daemon/firewall/chain/chain.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
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
|
||||
ListRules() ([]Rule, error)
|
||||
RemoveRuleByHandle(handle uint64) error
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type chain struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
chain string
|
||||
}
|
||||
|
||||
type NftOutput struct {
|
||||
Nftables []NftElement `json:"nftables"`
|
||||
}
|
||||
type NftElement struct {
|
||||
Rule *Rule `json:"rule,omitempty"`
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Handle uint64 `json:"handle"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
func (c *chain) AddRule(expr ...string) error {
|
||||
return c.nft.Rule().Add(c.family, c.table, c.chain, expr...)
|
||||
}
|
||||
|
||||
func (c *chain) ListRules() ([]Rule, error) {
|
||||
args := []string{"-a", "-j", "list", "chain", c.family.String(), c.table, c.chain}
|
||||
jsonData, err := c.nft.Command().RunWithOutput(args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var output NftOutput
|
||||
if err := json.Unmarshal([]byte(jsonData), &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rules []Rule
|
||||
for _, el := range output.Nftables {
|
||||
if el.Rule != nil {
|
||||
rules = append(rules, *el.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (c *chain) RemoveRuleByHandle(handle uint64) error {
|
||||
return c.nft.Rule().Delete(c.family, c.table, c.chain, handle)
|
||||
}
|
||||
|
||||
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,14 @@ type Chains interface {
|
||||
|
||||
NewLocalOutput() error
|
||||
LocalOutput() LocalOutput
|
||||
|
||||
NewLocalForward() error
|
||||
LocalForward() LocalForward
|
||||
|
||||
ClearRules() error
|
||||
|
||||
NewNoneChain(chain string) (Chain, error)
|
||||
NewChain(chain string, baseChain nftChain.ChainOptions) (Chain, error)
|
||||
}
|
||||
|
||||
type chains struct {
|
||||
@@ -31,8 +42,9 @@ type chains struct {
|
||||
forward Forward
|
||||
packetFilter PacketFilter
|
||||
|
||||
localInput LocalInput
|
||||
localOutput LocalOutput
|
||||
localInput LocalInput
|
||||
localOutput LocalOutput
|
||||
localForward LocalForward
|
||||
|
||||
family nftFamily.Type
|
||||
table string
|
||||
@@ -40,11 +52,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 +83,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 +97,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 +111,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 +150,47 @@ func (c *chains) NewLocalOutput() error {
|
||||
func (c *chains) LocalOutput() LocalOutput {
|
||||
return c.localOutput
|
||||
}
|
||||
|
||||
func (c *chains) NewLocalForward() error {
|
||||
localForward, err := newLocalForward(c.nft, c.family, c.table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.localForward = localForward
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chains) LocalForward() LocalForward {
|
||||
return c.localForward
|
||||
}
|
||||
|
||||
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: "",
|
||||
}
|
||||
|
||||
41
internal/daemon/firewall/chain/local_forward.go
Normal file
41
internal/daemon/firewall/chain/local_forward.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
)
|
||||
|
||||
type LocalForward interface {
|
||||
AddRule(expr ...string) error
|
||||
AddRuleIn(AddRuleFunc func(expr ...string) error) error
|
||||
}
|
||||
|
||||
type localForward struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
chain string
|
||||
}
|
||||
|
||||
func newLocalForward(nft nft.NFT, family family.Type, table string) (LocalForward, error) {
|
||||
chain := "local-forward"
|
||||
if err := nft.Chain().Add(family, table, chain, nftChain.TypeNone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &localForward{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
chain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *localForward) AddRule(expr ...string) error {
|
||||
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
|
||||
}
|
||||
|
||||
func (l *localForward) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
|
||||
return AddRuleFunc("iifname != \"lo\" counter jump " + l.chain)
|
||||
}
|
||||
@@ -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,22 @@ 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 err := f.reloadForwardAddIPs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -17,3 +27,53 @@ func (f *firewall) reloadForward() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *firewall) reloadForwardAddIPs() error {
|
||||
if err := f.chains.NewLocalForward(); err != nil {
|
||||
return err
|
||||
}
|
||||
chain := f.chains.LocalForward()
|
||||
if err := chain.AddRuleIn(f.chains.Forward().AddRule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ipConfig := range f.config.IP4.InIPs {
|
||||
if ipConfig.Action != ActionDrop && ipConfig.Action != ActionReject {
|
||||
continue
|
||||
}
|
||||
if err := forwardAddIP(chain.AddRule, ipConfig, "ip"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !f.config.IP6.Enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipConfig := range f.config.IP6.InIPs {
|
||||
if ipConfig.Action != ActionDrop && ipConfig.Action != ActionReject {
|
||||
continue
|
||||
}
|
||||
if err := forwardAddIP(chain.AddRule, ipConfig, "ip6"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func forwardAddIP(addRuleFunc func(expr ...string) error, config ConfigIP, ipMatch string) error {
|
||||
rule := ipMatch + " saddr " + config.IP + " iifname != \"lo\""
|
||||
|
||||
// There, during routing, the port changes and then the IP blocking rule will not work.
|
||||
//if !config.OnlyIP {
|
||||
// rule += " " + config.Protocol.String() + " dport " + strconv.Itoa(int(config.Port))
|
||||
//}
|
||||
|
||||
rule += " counter " + config.Action.String()
|
||||
if err := addRuleFunc(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return 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,17 @@
|
||||
"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.ssh.subject": "SSH login alert for user {{.User}} from {{.IP}}",
|
||||
"alert.login.ssh.body": "Logged into the OS via ssh:\n Time: {{.Time}}\n IP: {{.IP}}\n User: {{.User}}\n Log: {{.Log}}",
|
||||
|
||||
"alert.login.locale.subject": "Login message for user {{.User}} (TTY)",
|
||||
"alert.login.locale.body": "Logged into the OS via TTY:\n Time: {{.Time}}\n User: {{.User}}\n Log: {{.Log}}",
|
||||
|
||||
"alert.login.su.subject": "User {{.ByUser}} has accessed user {{.User}} via su",
|
||||
"alert.login.su.body": "User {{.ByUser}} accessed user {{.User}} via su.\nTime: {{.Time}}\nLog: {{.Log}}",
|
||||
|
||||
"alert.login.sudo.subject": "User {{.ByUser}} has accessed user {{.User}} via sudo",
|
||||
"alert.login.sudo.body": "User {{.ByUser}} accessed user {{.User}} via sudo.\nTime: {{.Time}}\nLog: {{.Log}}"
|
||||
}
|
||||
|
||||
@@ -25,5 +25,17 @@
|
||||
"daemon stopped": "Жын тоқтатылды",
|
||||
"daemon stop failed": "Жынды тоқтату сәтсіз аяқталды",
|
||||
"daemon is not running": "Демон жұмыс істемейді",
|
||||
"daemon is not reopening logger": "Жын журналды қайта ашпады"
|
||||
"daemon is not reopening logger": "Жын журналды қайта ашпады",
|
||||
|
||||
"alert.login.ssh.subject": "{{.IP}} IP мекенжайынан {{.User}} пайдаланушысына арналған SSH кіру хабарламасы",
|
||||
"alert.login.ssh.body": "ОС-қа ssh арқылы кірді:\n Уақыт: {{.Time}}\n IP: {{.IP}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}",
|
||||
|
||||
"alert.login.locale.subject": "{{.User}} пайдаланушысына арналған кіру хабарламасы (TTY)",
|
||||
"alert.login.locale.body": "ОЖ-ға TTY арқылы кірдіңіз:\n Уақыт: {{.Time}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}",
|
||||
|
||||
"alert.login.su.subject": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына su арқылы кіру мүмкіндігін алды",
|
||||
"alert.login.su.body": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына su арқылы кірді.\nУақыты: {{.Time}}\nЛог: {{.Log}}",
|
||||
|
||||
"alert.login.sudo.subject": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына sudo арқылы кіру мүмкіндігін алды",
|
||||
"alert.login.sudo.body": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына sudo арқылы кірді.\nУақыты: {{.Time}}\nЛог: {{.Log}}"
|
||||
}
|
||||
@@ -25,5 +25,17 @@
|
||||
"daemon stopped": "Демон остановлен",
|
||||
"daemon stop failed": "Остановка демона не удалась",
|
||||
"daemon is not running": "Демон не запущен",
|
||||
"daemon is not reopening logger": "Демон не открыл журнал повторно"
|
||||
"daemon is not reopening logger": "Демон не открыл журнал повторно",
|
||||
|
||||
"alert.login.ssh.subject": "SSH-сообщение о входе пользователя {{.User}} с IP-адреса {{.IP}}",
|
||||
"alert.login.ssh.body": "Вошли в ОС через ssh:\n Время: {{.Time}}\n IP: {{.IP}}\n Пользователь: {{.User}}\n Лог: {{.Log}}",
|
||||
|
||||
"alert.login.locale.subject": "Сообщение о входе пользователя {{.User}} (TTY)",
|
||||
"alert.login.locale.body": "Вошли в ОС через TTY:\n Время: {{.Time}}\n Пользователь: {{.User}}\n Лог: {{.Log}}",
|
||||
|
||||
"alert.login.su.subject": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через su",
|
||||
"alert.login.su.body": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через su.\nВремя: {{.Time}}\nЛог: {{.Log}}",
|
||||
|
||||
"alert.login.sudo.subject": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через sudo",
|
||||
"alert.login.sudo.body": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через sudo.\nВремя: {{.Time}}\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
|
||||
}
|
||||
41
internal/setting/analyzer/login.go
Normal file
41
internal/setting/analyzer/login.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package analyzer
|
||||
|
||||
type Login struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Notify bool `mapstructure:"notify"`
|
||||
|
||||
SSHEnable bool `mapstructure:"ssh_enable"`
|
||||
SSHNotify bool `mapstructure:"ssh_notify"`
|
||||
|
||||
LocalEnable bool `mapstructure:"local_enable"`
|
||||
LocalNotify bool `mapstructure:"local_notify"`
|
||||
|
||||
SuEnable bool `mapstructure:"su_enable"`
|
||||
SuNotify bool `mapstructure:"su_notify"`
|
||||
|
||||
SudoEnable bool `mapstructure:"sudo_enable"`
|
||||
SudoNotify bool `mapstructure:"sudo_notify"`
|
||||
}
|
||||
|
||||
func defaultLogin() Login {
|
||||
return Login{
|
||||
Enabled: true,
|
||||
Notify: true,
|
||||
|
||||
SSHEnable: true,
|
||||
SSHNotify: true,
|
||||
|
||||
LocalEnable: true,
|
||||
LocalNotify: true,
|
||||
|
||||
SuEnable: true,
|
||||
SuNotify: true,
|
||||
|
||||
SudoEnable: false,
|
||||
SudoNotify: 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",
|
||||
}
|
||||
}
|
||||
|
||||
66
internal/setting/docker/docker.go
Normal file
66
internal/setting/docker/docker.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"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/setting/validate"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
RuleStrategy string `mapstructure:"rule_strategy"`
|
||||
}
|
||||
|
||||
func InitSetting(path string) (Setting, error) {
|
||||
if err := validate.IsTomlFile(path, "otherSettingsPath.docker"); 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,
|
||||
RuleStrategy: "incremental",
|
||||
}
|
||||
}
|
||||
|
||||
func (s Setting) Validate() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Setting) ToRuleStrategy() (docker_monitor.RuleStrategy, error) {
|
||||
switch s.RuleStrategy {
|
||||
case "rebuild":
|
||||
return docker_monitor.RuleStrategyRebuild, nil
|
||||
case "incremental":
|
||||
return docker_monitor.RuleStrategyIncremental, nil
|
||||
}
|
||||
|
||||
return docker_monitor.RuleStrategyRebuild, errors.New("invalid option rule_strategy. Must be rebuild or incremental")
|
||||
}
|
||||
@@ -4,10 +4,12 @@ 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"`
|
||||
@@ -17,6 +19,7 @@ type options struct {
|
||||
|
||||
func defaultOptions() options {
|
||||
return options{
|
||||
ClearMode: "global",
|
||||
SavesRules: false,
|
||||
SavesRulesPath: "/etc/nftables.conf",
|
||||
DnsStrict: false,
|
||||
@@ -45,3 +48,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,21 +1,37 @@
|
||||
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/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/i18n"
|
||||
analyzerSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/analyzer"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/docker"
|
||||
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"`
|
||||
Docker string `mapstructure:"docker"`
|
||||
}
|
||||
|
||||
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",
|
||||
Docker: "/etc/kor-elf-shield/docker.toml",
|
||||
}
|
||||
}
|
||||
|
||||
func (o *otherSettingsPath) ToFirewallConfig() (firewall.Config, error) {
|
||||
func (o *otherSettingsPath) ToFirewallConfig(dockerSupport bool) (firewall.Config, error) {
|
||||
setting, err := firewallSetting.InitSetting(o.Firewall)
|
||||
if err != nil {
|
||||
return firewall.Config{}, err
|
||||
@@ -36,6 +52,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 +76,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: dockerSupport,
|
||||
},
|
||||
MetadataNaming: firewall.ConfigMetadata{
|
||||
TableName: setting.MetadataNaming.TableName,
|
||||
@@ -70,3 +93,113 @@ 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,
|
||||
},
|
||||
Local: config.LoginLocal{
|
||||
Enabled: setting.Login.LocalEnable,
|
||||
Notify: setting.Login.LocalNotify,
|
||||
},
|
||||
Su: config.LoginSu{
|
||||
Enabled: setting.Login.SuEnable,
|
||||
Notify: setting.Login.SuNotify,
|
||||
},
|
||||
Sudo: config.LoginSudo{
|
||||
Enabled: setting.Login.SudoEnable,
|
||||
Notify: setting.Login.SudoNotify,
|
||||
},
|
||||
}
|
||||
|
||||
return config.Config{
|
||||
BinPath: binPath,
|
||||
Login: login,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *otherSettingsPath) ToDockerConfig(binaryLocations *binaryLocations) (config docker_monitor.Config, dockerSupport bool, err error) {
|
||||
if binaryLocations.Docker == "" {
|
||||
return docker_monitor.Config{}, false, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||
"Parameter": "binaryLocations.docker",
|
||||
}))
|
||||
}
|
||||
|
||||
setting, err := docker.InitSetting(o.Docker)
|
||||
if err != nil {
|
||||
return docker_monitor.Config{}, false, err
|
||||
}
|
||||
|
||||
if err := setting.Validate(); err != nil {
|
||||
return docker_monitor.Config{}, false, err
|
||||
}
|
||||
|
||||
ruleStrategy, err := setting.ToRuleStrategy()
|
||||
if err != nil {
|
||||
return docker_monitor.Config{}, false, err
|
||||
}
|
||||
|
||||
return docker_monitor.Config{
|
||||
Path: binaryLocations.Docker,
|
||||
RuleStrategy: ruleStrategy,
|
||||
}, setting.Enabled, nil
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func settingDefault() *setting {
|
||||
}
|
||||
}
|
||||
|
||||
func (s setting) ToDaemonOptions() (daemon.DaemonOptions, error) {
|
||||
func (s setting) ToDaemonOptions(dockerSupport bool) (daemon.DaemonOptions, error) {
|
||||
if s.PidFile == "" {
|
||||
return daemon.DaemonOptions{}, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||
"Parameter": "pid_file",
|
||||
@@ -56,7 +56,12 @@ func (s setting) ToDaemonOptions() (daemon.DaemonOptions, error) {
|
||||
}))
|
||||
}
|
||||
|
||||
firewallConfig, err := s.OtherSettingsPath.ToFirewallConfig()
|
||||
firewallConfig, err := s.OtherSettingsPath.ToFirewallConfig(dockerSupport)
|
||||
if err != nil {
|
||||
return daemon.DaemonOptions{}, err
|
||||
}
|
||||
|
||||
analyzerConfig, err := s.OtherSettingsPath.ToAnalyzerConfig(s.BinaryLocations)
|
||||
if err != nil {
|
||||
return daemon.DaemonOptions{}, err
|
||||
}
|
||||
@@ -66,6 +71,7 @@ func (s setting) ToDaemonOptions() (daemon.DaemonOptions, error) {
|
||||
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