Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3e44359d1 | ||
|
|
b782b7c975
|
||
|
|
e28fafc1e9
|
||
|
|
350c78c40a
|
||
|
|
22d7f901d0
|
||
|
|
1cff4fb2ba
|
||
|
|
2d219edd23
|
||
|
|
1cc52ca28e
|
||
|
|
7392e753ea
|
||
|
|
6148b0a06d
|
||
|
|
7322455ae0
|
||
|
|
232b63a207
|
||
|
|
4d4e7272ce
|
||
|
|
5c2b34b742
|
||
|
|
5315a4e0ff
|
||
|
|
0286ebc141
|
||
|
|
9759d9805b
|
||
|
|
29df5f3fd8
|
||
|
|
f2a9dc41be
|
||
|
|
f2dfeb507d
|
||
|
|
bae20cbf49 | ||
|
|
f9c438c973
|
||
|
|
cbf3dd6402
|
||
|
|
8495d7c4e2
|
||
|
|
311add0e8b
|
||
|
|
c26a898d05
|
||
|
|
9d5b6c7d6b
|
||
|
|
a0893ea361
|
||
|
|
0111304d85
|
||
|
|
aa0c5a9551
|
||
|
|
65806d701a
|
||
|
|
8c10ab0e62
|
||
|
|
95aec171e0
|
||
|
|
666f563ef5
|
||
|
|
ed7bd905f8
|
||
|
|
d06dd6e9fc
|
||
|
|
f650e89610
|
||
|
|
8c55ad1b23
|
||
|
|
17efaaa36a
|
||
|
|
df469a6f6d
|
||
|
|
6de78c2708
|
||
|
|
87add28edd
|
||
|
|
7cdd761dcd | ||
|
|
bdbbd303cc
|
||
|
|
b7cfc756c1
|
||
|
|
b396963976
|
||
|
|
eb4b0b4e4f
|
||
|
|
03a1d46f1b
|
||
|
|
eda1b956f3
|
||
|
|
14c4c504fe
|
||
|
|
5acc8a72de
|
||
|
|
1047c75629
|
||
|
|
f93716e194
|
||
|
|
72a0f941a1 | ||
|
|
e9cd163ae7
|
||
|
|
f778e25575
|
||
|
|
21ab1802fe
|
||
|
|
62517e6f4c
|
||
|
|
8a7608dac6
|
||
|
|
8d90a3770d
|
||
|
|
4acb81b5a7
|
||
|
|
49ab9c48c7
|
||
|
|
28019ec171
|
||
|
|
03b4009f96
|
||
|
|
71502ff0c9
|
||
|
|
ab59b356dc
|
||
|
|
8595c6791d
|
||
|
|
c64c94dceb
|
||
|
|
a387e85569
|
||
|
|
ab8466ada2
|
||
|
|
d978343f4c
|
||
|
|
14a6b9df0b
|
||
|
|
298c7140a4
|
||
|
|
527b6c8264
|
||
|
|
0d707ac3c6
|
||
|
|
b535195c1f
|
||
|
|
3c12429f0e
|
||
|
|
25ee39c0ec
|
||
|
|
95e1f274f7
|
||
|
|
5f72efd1bf
|
||
|
|
e8826cb86b
|
||
|
|
f12097b280
|
||
|
|
3c040945bc
|
||
|
|
0fdc07c0af
|
||
|
|
a1345bd3e1
|
||
|
|
d2f3640b75
|
||
|
|
1363ff4bef
|
||
|
|
ec362f3c9a | ||
|
|
4c2d11423d
|
||
|
|
671346e735
|
||
|
|
b500d9fe57
|
||
|
|
64c44085ea
|
||
|
|
88264e1f4f
|
||
|
|
954f3b4ce2
|
||
|
|
9298f09b2e
|
||
|
|
086aa784ea
|
||
|
|
96ded2fc43
|
||
|
|
6586e876d9
|
||
|
|
90a8374c93
|
||
|
|
0352bf7dd7
|
||
|
|
d249bcdf16
|
||
|
|
e0395f6dc1
|
||
|
|
ab4496f6b8
|
||
|
|
a084fef3d2
|
||
|
|
9a9899958b
|
||
|
|
9bfabd2148
|
||
|
|
1298685ca4
|
||
|
|
0ae0399aec | ||
|
|
a87eade21c
|
||
|
|
447755dcc0
|
||
|
|
6271682e4b
|
||
|
|
c3571259a0
|
||
|
|
9a406bedb6
|
||
|
|
d25932ef7d
|
||
|
|
a31386ed10
|
||
|
|
9de460d2c9
|
||
|
|
2a2ec666e6
|
||
|
|
f198ec2c2c
|
||
|
|
12bdd9ca3e
|
||
|
|
d796b3a61b
|
||
|
|
a49bf15023
|
||
|
|
9597257a07
|
||
|
|
bfcaca27a9
|
||
|
|
14168d3765
|
||
|
|
4587b522be
|
||
|
|
d2e5db7f66
|
||
|
|
eaa3513e03
|
||
|
|
6d35c3e5bf
|
||
|
|
3061d0f31e
|
||
|
|
5e0fb5787a
|
||
|
|
c0573c4e36
|
||
|
|
8468fe851b
|
||
|
|
6438358a53
|
||
|
|
489c5c0cbe
|
||
|
|
7c15813b0e
|
||
|
|
3515b66dc7
|
||
|
|
f5ff0c1afd | ||
|
|
63bc845b8b
|
||
|
|
a44f9b4e75
|
||
|
|
4647d1303e
|
||
|
|
221fdb8d3b
|
||
|
|
a7e4c7d750
|
||
|
|
75c8eba0cd
|
||
|
|
bf8711aadd
|
||
|
|
1dbb4d0bff
|
||
|
|
993f48f541
|
||
|
|
286f32b618
|
||
|
|
42e4a8cf40
|
||
|
|
be3861ee6e
|
||
|
|
d0a358a445
|
||
|
|
39cfb8a7b6
|
||
|
|
65eaa37637
|
||
|
|
c4852c3540 | ||
|
|
b884494250
|
||
|
|
598d83d6da
|
||
|
|
f737edc3ce
|
||
|
|
dc85bc759a
|
||
|
|
93b2927da7
|
||
|
|
afb0773dfd
|
||
|
|
187c447301
|
||
|
|
3ec6b4c72d
|
||
|
|
b63e3adbd3
|
||
|
|
aa519c8b44
|
||
|
|
8329da32e3
|
||
|
|
833bc394b3
|
||
|
|
e422bc4206
|
||
|
|
47aa0a9d6c
|
||
|
|
58dbee450a
|
||
|
|
68034fd6f9
|
||
|
|
7b77b8730e
|
||
|
|
187e874c29
|
||
|
|
ee5a6a2d3d
|
||
|
|
38283247e9
|
||
|
|
79c7ef1f91
|
||
|
|
e29d0de632
|
||
|
|
be082a1841
|
||
|
|
4b364cbdf0
|
||
|
|
dfa23bc7a6
|
||
|
|
3a34569e78
|
||
|
|
b1f5ce4e9b
|
||
|
|
f2d851baa7
|
||
|
|
2a617b5c17
|
||
|
|
a648647e4a
|
||
|
|
6b482a350b
|
||
|
|
097cf362e3
|
||
|
|
bf7d463930
|
||
|
|
b49889ef58
|
||
|
|
fd899087d4
|
||
|
|
8f254d11c1
|
||
|
|
2e08bf6b6a
|
||
|
|
036f037a30
|
||
|
|
c7f25b4ba8
|
||
|
|
623d626878
|
||
|
|
e1bace602c
|
||
|
|
e85fd785cd
|
||
|
|
c6841d14f3
|
||
|
|
57b80da767
|
||
|
|
696961f7c0
|
||
|
|
af082f36da
|
||
|
|
a889e5c81a
|
||
|
|
99e155fe10
|
||
|
|
2fffe45a89
|
||
|
|
ff0317ed0b
|
||
|
|
0b627a283d
|
||
|
|
2b8a3e0d98
|
||
|
|
c09bf01de1
|
||
|
|
627b70e024
|
||
|
|
660e1fcebd
|
||
|
|
c9093f8244
|
||
|
|
8985ff884d
|
||
|
|
c7dadb3684
|
||
|
|
d5e92b70ef
|
||
|
|
3bbedc5088 | ||
|
|
960494eec0
|
||
|
|
98a62b4551
|
||
|
|
0fa8d88479
|
||
|
|
9eef81d1a5
|
||
|
|
6821924c8e
|
||
|
|
f0958a340f
|
||
|
|
d9a40c620c
|
||
|
|
fd764fb5c5
|
||
|
|
d6af8a7ea5
|
||
|
|
f0d5b597cb
|
||
|
|
81a28bf485
|
||
|
|
0fb8c0b42d
|
||
|
|
6b79928b3a
|
+209
-4
@@ -1,4 +1,209 @@
|
||||
## 0.5.0 (17.1.2026)
|
||||
## 0.14.0 (05.09.2026)
|
||||
#### Русский
|
||||
* Исправлена ошибка в правилах исходящего ICMP: при `icmp_timestamp_drop=true` теперь корректно блокируются `timestamp-reply` пакеты (вместо `timestamp-request`).
|
||||
* Добавлена новая команда `kor-elf-shield config test`. Теперь перед перезапуском демона можно проверить корректность конфигурации.
|
||||
***
|
||||
#### English
|
||||
* Fixed an issue in outbound ICMP rules: when `icmp_timestamp_drop=true`, `timestamp-reply` packets are now correctly blocked (instead of `timestamp-request`).
|
||||
* Added a new command, `kor-elf-shield config test`. Now you can check the configuration before restarting the daemon.
|
||||
***
|
||||
## 0.13.0 (16.08.2026)
|
||||
#### Русский
|
||||
* Обновления критических ошибок:
|
||||
* Исправлена ошибка, из-за которой неудачные попытки авторизации по SSH не обрабатывались, если указанного пользователя не существовало в системе.
|
||||
* Исправлена ошибка, из-за которой неудачные попытки аутентификации по SSH-ключу не обрабатывались.
|
||||
<br>**Примечание:** По умолчанию SSH-сервер не регистрирует неудачные попытки входа с использованием ключа. Чтобы включить отслеживание неудачных попыток входа с использованием ключа, необходимо добавить параметр `LogLevel VERBOSE` в настройки `sshd_config`.
|
||||
* Добавлено: поддержка исключений IP-адресов в настройках обработки blocklist.
|
||||
* Теперь можно добавлять параметр `exclude_ips` в конфигурационный файл `blocklists.toml` чтобы исключать определённые диапазоны IP-адресов из обработки. По умолчанию: `["127.0.0.1/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7",]`.
|
||||
* Реализована простая проверка на наличия таблицы в NFTables.
|
||||
* Добавлены данные для проверки наличия таблицы в NFTables в команде `kor-elf-shield status`.
|
||||
* Добавлена новая группа параметров `[rulesGuard]` в файл `firewall.toml`:
|
||||
* `enabled` — Включает мониторинг правил NFTables. По умолчанию: `true`.
|
||||
* `notifications` - Включает уведомления в случае проблем с правилами NFTables. По умолчанию: `true`.
|
||||
* `recovery` - Восстанавливает правила NFTables в случае проблем. По умолчанию: `true`.
|
||||
* `interval` - Интервал проверки правил NFTables в секундах. По умолчанию: `3600`.
|
||||
* Добавлена поддержка разделения `patterns` на партиции в настройках `analyzer.toml`.
|
||||
* Теперь правила анализа логов и защиты от перебора пароля могут обрабатывать шаблоны по партициям.
|
||||
* Это помогает гибче группировать шаблоны и снижать пересечения между независимыми наборами правил.
|
||||
***
|
||||
#### English
|
||||
* Critical bug fixes:
|
||||
* Fixed an issue where failed SSH login attempts were not processed if the specified user did not exist on the system.
|
||||
* Fixed a bug where unsuccessful SSH key authentication attempts were not processed.
|
||||
<br>**Note:** By default, the SSH server does not log unsuccessful login attempts using a key. To enable tracking of unsuccessful login attempts using a key, you must add the `LogLevel VERBOSE` parameter to the `sshd_config` settings.
|
||||
* Added: Support for IP address exclusions in blocklist processing settings.
|
||||
* You can now add the `exclude_ips` parameter to the `blocklists.toml` configuration file to exclude specific IP address ranges from processing. Default: `["127.0.0.1/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7",]`.
|
||||
* Implemented a simple check for the presence of a table in NFTables.
|
||||
* Added check data to `kor-elf-shield status` command for the presence of a table in NFTables.
|
||||
* Added a new `[rulesGuard]` parameter group to the `firewall.toml` file:
|
||||
* `enabled` - Enables NFTables rules monitoring. Default: `true`.
|
||||
* `notifications` - Enables notifications in case of problems with NFTables rules. Default: `true`.
|
||||
* `recovery` - Recovers NFTables rules in case of problems. Default: `true`.
|
||||
* `interval` - NFTables rules checking interval in seconds. Default: `3600`.
|
||||
* Added partitioning support for `patterns` in the `analyzer.toml` configuration.
|
||||
* Log analysis and brute-force protection rules can now process patterns by partitions.
|
||||
* This helps organize patterns more flexibly and reduces overlap between independent rule sets.
|
||||
***
|
||||
## 0.12.0 (17.06.2026)
|
||||
#### Русский
|
||||
* Добавлено: отображение номера уведомления в групповых уведомлениях.
|
||||
* Добавлено: теперь в уведомлениях отображается количество блокировок IP-адреса.
|
||||
* Добавлены параметры для ограничения частоты уведомлений об успешной блокировке (`analyzer.toml`):
|
||||
* `ssh_notify_cooldown_seconds` — задаёт минимальный интервал в секундах между уведомлениями об успешных SSH-блокировках.
|
||||
* `ssh_notify_every` — отправляет следующее уведомление об успешной SSH-блокировке только после указанного количества новых сообщений о блокировке; имеет приоритет над `ssh_notify_cooldown_seconds`.
|
||||
* `notify_cooldown_seconds` — задаёт минимальный интервал в секундах между уведомлениями об успешных блокировках для пользовательских правил защиты от перебора пароля.
|
||||
* `notify_every` — отправляет следующее уведомление для пользовательского правила только после указанного количества новых сообщений о блокировке; имеет приоритет над `notify_cooldown_seconds`.
|
||||
***
|
||||
#### English
|
||||
* Added: display of notification number in group notifications.
|
||||
* Added: notifications now display how many times an IP address has been blocked.
|
||||
* Added options to limit the frequency of notifications about successful blocking (`analyzer.toml`):
|
||||
* `ssh_notify_cooldown_seconds` — Sets the minimum interval in seconds between notifications of successful SSH locks.
|
||||
* `ssh_notify_every` — Sends the next successful SSH lock notification only after the specified number of new lock messages; takes precedence over `ssh_notify_cooldown_seconds`.
|
||||
* `notify_cooldown_seconds` — Sets the minimum interval in seconds between notifications of successful locks for custom password attack protection rules.
|
||||
* `notify_every` — Sends the next notification for the custom rule only after the specified number of new block messages; takes precedence over `notify_cooldown_seconds`.
|
||||
***
|
||||
## 0.11.0 (07.05.2026)
|
||||
#### Русский
|
||||
* В настройки файла `firewall.toml` добавлен параметр `options.cache`.
|
||||
* Этот параметр включает кэширование, чтобы избежать постоянной компиляции команд nftables во временный файл. Файл кэша изменяется после изменения настроек или обновления версии программы. (`Включено по умолчанию`)
|
||||
* Логика добавления правил в nftables была переработана.
|
||||
* Команды теперь собираются во временном файле.
|
||||
* Запрос выполняется с использованием параметра -f.
|
||||
* Переработана логика обновления данных по блокировке IP адресов, которые получаем через другие сервисы.
|
||||
* Список IP адресов собирается в файл.
|
||||
* Запрос выполняется с использованием параметра -f.
|
||||
* Обновлена версия go-nftables-client до v0.2.1.
|
||||
* Улучшен вывод `Uptime` в команде `kor-elf-shield status`.
|
||||
* Улучшен вывод времени блокировки в уведомлениях.
|
||||
***
|
||||
#### English
|
||||
* The `options.cache` parameter has been added to the `firewall.toml` file settings.
|
||||
* This parameter enables caching to avoid constantly compiling nftables commands into a temporary file. The cache file changes after changing settings or updating the program version. (`Enabled by default`)
|
||||
* The logic for adding rules to nftables has been reworked.
|
||||
* Commands are now collected in a temporary file.
|
||||
* The query is executed using the -f parameter.
|
||||
* The logic for updating data on blocking IP addresses obtained through other services has been reworked.
|
||||
* The list of IP addresses is collected into a file.
|
||||
* The query is executed using the -f parameter.
|
||||
* Updated go-nftables-client to v0.2.1.
|
||||
* Improved `Uptime` output in `kor-elf-shield status` command.
|
||||
* Improved display of blocking time in notifications.
|
||||
***
|
||||
## 0.10.0 (12.04.2026)
|
||||
#### Русский
|
||||
* При автоматической блокировке добавил возможность получать данные об IP-адресах (континент, страна, город, часовой пояс).
|
||||
* В файл analyzer.toml добавлен параметр type к [[logAlert.rules.patterns.values]] в котором можно указать тип "ip". Это позволит для этого поля получить данные об IP-адресе при отправке оповещения.
|
||||
* Для получения данных об IP-адресах можно вызвать команду `kor-elf-shield geoip info <ip_address>`.
|
||||
* Можно принудительно обновить базу geoip командой `kor-elf-shield geoip refresh`.
|
||||
* Исправлена ошибка, когда в уведомлениях приходили лишние записи logs.
|
||||
* Улучшен вывод информации в комманде `kor-elf-shield status`.
|
||||
* В настройки файла kor-elf-shield.toml добавлен параметр otherSettingsPath.geoip.
|
||||
* Добавлен новый файл настроек geoip.toml. В этом файле настраиваются параметры для получения данных об IP-адресах.
|
||||
***
|
||||
#### English
|
||||
* Added the ability to receive data on IP addresses (continent, country, city, time zone) during automatic blocking.
|
||||
* The analyzer.toml file now has a new parameter, type, added to [[logAlert.rules.patterns.values]], allowing you to specify the "ip" type. This will allow this field to retrieve IP address data when sending an alert.
|
||||
* To obtain IP address data, you can use the `kor-elf-shield geoip info <ip_address>` command.
|
||||
* You can force a geoip database update with the `kor-elf-shield geoip refresh` command.
|
||||
* Fixed a bug where notifications contained extra logs.
|
||||
* Improved output of information in the `kor-elf-shield status` command.
|
||||
* The otherSettingsPath.geoip parameter has been added to the kor-elf-shield.toml file.
|
||||
* A new geoip.toml settings file has been added. This file configures parameters for retrieving IP address data.
|
||||
***
|
||||
## 0.9.0 (21.03.2026)
|
||||
#### Русский
|
||||
* Добавилась поддержка Port knocking.
|
||||
* В firewall.toml добавился раздел Port knocking.
|
||||
* Теперь вы можете получить список IP-адресов от различных сервисов для блокировки доступа:
|
||||
* Spamhaus Don't Route Or Peer Lists
|
||||
* DShield.org Recommended Block List
|
||||
* TOR Exit Nodes List
|
||||
* Project Honey Pot Directory of Dictionary Attacker IPs
|
||||
* C.I. Army Malicious IP List
|
||||
* BruteForceBlocker IP List
|
||||
* Blocklist.de
|
||||
* Stop Forum Spam
|
||||
* GreenSnow Hack List
|
||||
* В настройки файла kor-elf-shield.toml добавлен параметр otherSettingsPath.blocklists.
|
||||
* Добавлен новый файл настроек, blocklists.toml. Он содержит параметры для получения списка IP-адресов для блокировки.
|
||||
***
|
||||
#### English
|
||||
* Added support for port knocking.
|
||||
* A Port knocking section has been added to firewall.toml.
|
||||
* Now you can get a list of IP addresses from various services to block access:
|
||||
* Spamhaus Don't Route Or Peer Lists
|
||||
* DShield.org Recommended Block List
|
||||
* TOR Exit Nodes List
|
||||
* Project Honey Pot Directory of Dictionary Attacker IPs
|
||||
* C.I. Army Malicious IP List
|
||||
* BruteForceBlocker IP List
|
||||
* Blocklist.de
|
||||
* Stop Forum Spam
|
||||
* GreenSnow Hack List
|
||||
* Added the otherSettingsPath.blocklists parameter to the kor-elf-shield.toml settings.
|
||||
* Added a new settings file, blocklists.toml. It contains settings for obtaining a list of IP addresses to block.
|
||||
## 0.8.0 (09.03.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Теперь можно тонко настроить блокировку портов для IP адреса, который пытается подобрать пароль.
|
||||
* В файл настроек analyzer.toml в [[bruteForceProtection.groups]] добавлен новый параметр "block_type".
|
||||
* В файл настроек analyzer.toml в [[bruteForceProtection.groups]] добавлен новый параметр "ports".
|
||||
* В файл настроек analyzer.toml в [[bruteForceProtection.groups.rate_limits]] добавлен новый параметр "block_type".
|
||||
* В файл настроек analyzer.toml в [[bruteForceProtection.groups.rate_limits]] добавлен новый параметр "ports".
|
||||
* Смотрите полный список по ссылке: https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/commit/d0a358a445b1dec850d8b84c06e86bd6872796cf/assets/configs/analyzer.toml
|
||||
* Команда `kor-elf-shield ban clear` была переименованна в `kor-elf-shield block clear`.
|
||||
* Добавлена команда `kor-elf-shield block add`. Через эту команду можно заблокировать IP адрес. Смотрите подробно в `kor-elf-shield block add --help`.
|
||||
* Добавлена команда `kor-elf-shield block delete`. Через эту команду можно удалить заблокированный IP адрес. Смотрите подробно в `kor-elf-shield block delete --help`.
|
||||
***
|
||||
#### English
|
||||
* You can now fine-tune port blocking for the IP address attempting to brute-force a password.
|
||||
* A new "block_type" parameter has been added to the analyzer.toml settings file in [[bruteForceProtection.groups]].
|
||||
* A new "ports" parameter has been added to the analyzer.toml settings file in [[bruteForceProtection.groups]].
|
||||
* A new "block_type" parameter has been added to the analyzer.toml settings file in [[bruteForceProtection.groups.rate_limits]].
|
||||
* A new "ports" parameter has been added to the analyzer.toml settings file in [[bruteForceProtection.groups.rate_limits]].
|
||||
* See the full list at: https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/commit/d0a358a445b1dec850d8b84c06e86bd6872796cf/assets/configs/analyzer.toml
|
||||
* The `kor-elf-shield ban clear` command has been renamed to `kor-elf-shield block clear`.
|
||||
* The `kor-elf-shield block add` command has been added. This command can be used to block an IP address. See `kor-elf-shield block add --help` for details.
|
||||
* The `kor-elf-shield block delete` command has been added. This command can be used to delete a blocked IP address. See `kor-elf-shield block delete --help` for details.
|
||||
***
|
||||
## 0.7.0 (28.02.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Добавлена возможность настройки отслеживания событий в журналах.
|
||||
* Добавлены настройки для защиты от перебора паролей.
|
||||
* В файл настроек analyzer.toml добавлены новые параметры. Смотрите полный список по ссылке: https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/commit/187c447301b9c0bfa41ec2b2c9435ab0ce44bed6/assets/configs/analyzer.toml
|
||||
* Добавлена команда `kor-elf-shield ban clear`, которая разблокирует все IP адреса. Которые были забанены.
|
||||
***
|
||||
#### English
|
||||
* Added the ability to customize event tracking in logs.
|
||||
* Added settings to protect against password guessing.
|
||||
* New parameters have been added to the analyzer.toml settings file. See the full list at: https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/commit/187c447301b9c0bfa41ec2b2c9435ab0ce44bed6/assets/configs/analyzer.toml
|
||||
* Added the `kor-elf-shield ban clear` command, which unbans all banned IP addresses.
|
||||
***
|
||||
## 0.6.0 (08.02.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Добавлена возможность повторной отправки уведомления, если в прошлый раз произошла ошибка.
|
||||
* Добавлена команда `kor-elf-shield notifications queue count`, которая возвращает количество уведомлений в очереди в базе данных.
|
||||
* Добавлена команда `kor-elf-shield notifications queue clear`, которая удаляет все уведомления из очереди в базе данных.
|
||||
* В файл настроек kor-elf-shield.toml добавлены новые параметры:
|
||||
* data_dir = Каталог для постоянных данных приложения (state): локальная база данных, кэш/индексы, файлы состояния и другие служебные файлы. Должен быть доступен на запись пользователю, от имени которого запущен демон. Если каталог не существует — будет создан. По умолчанию: "/var/lib/kor-elf-shield/"
|
||||
* В файл настроек notifications.toml добавлены новые параметры:
|
||||
* enable_retries = Включает повторные попытки отправить уведомление, если сразу не получилось. По умолчанию: true
|
||||
* retry_interval = Интервал времени в секундах между попытками. По умолчанию: 600
|
||||
***
|
||||
#### English
|
||||
* Added the ability to retry sending a notification if an error occurred the previous time.
|
||||
* Added the `kor-elf-shield notifications queue count` command, which returns the number of notifications in the queue in the database.
|
||||
* Added the `kor-elf-shield notifications queue clear` command, which removes all notifications from the queue in the database.
|
||||
* New parameters have been added to the kor-elf-shield.toml settings file:
|
||||
* data_dir = Directory for persistent application data (state): local database, cache/indexes, state files, and other internal data. Must be writable by the daemon user. If the directory does not exist, it will be created. Default: "/var/lib/kor-elf-shield/"
|
||||
* New parameters have been added to the notifications.toml settings file:
|
||||
* enable_retries = Enables repeated attempts to send a notification if the first attempt fails. Default: true
|
||||
* retry_interval = The time interval in seconds between attempts. Default: 600
|
||||
***
|
||||
## 0.5.0 (17.01.2026)
|
||||
***
|
||||
#### Русский
|
||||
* В настройках analyzer.toml добавил параметры local_enable и local_notify.
|
||||
@@ -22,7 +227,7 @@
|
||||
* 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)
|
||||
## 0.4.0 (11.01.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Удалён параметр options.docker_support из файла firewall.toml. Настройки от Docker перенесены в файл docker.toml.
|
||||
@@ -44,7 +249,7 @@
|
||||
* 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)
|
||||
## 0.3.0 (04.01.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Добавлена частичная поддержка Docker.
|
||||
@@ -85,7 +290,7 @@
|
||||
* 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)
|
||||
## 0.1.0 (08.11.2025)
|
||||
***
|
||||
#### Русский
|
||||
* Реализована возможность настраивать nftables:
|
||||
|
||||
+188
-3
@@ -12,6 +12,34 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
git.kor-elf.net/kor-elf-shield/blocklist
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Leonid Nikitin (kor-elf)
|
||||
|
||||
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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
git.kor-elf.net/kor-elf-shield/geoip2
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Leonid Nikitin (kor-elf)
|
||||
|
||||
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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
github.com/fsnotify/fsnotify
|
||||
|
||||
Copyright © 2012 The Go Authors. All rights reserved.
|
||||
@@ -92,6 +120,72 @@ THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
github.com/nxadm/tail
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# © Copyright 2015 Hewlett Packard Enterprise Development LP
|
||||
Copyright (c) 2014 ActiveState
|
||||
|
||||
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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
github.com/oschwald/geoip2-golang/v2
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2015, Gregory J. Oschwald <oschwald@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
github.com/oschwald/maxminddb-golang/v2
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2015, Gregory J. Oschwald <oschwald@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
github.com/pelletier/go-toml/v2
|
||||
|
||||
The bulk of github.com/pelletier/go-toml is distributed under the MIT license
|
||||
@@ -710,6 +804,31 @@ SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
go.etcd.io/bbolt
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Ben Johnson
|
||||
|
||||
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.
|
||||
@@ -773,17 +892,49 @@ starting in 2011 when the project was ported over:
|
||||
- internal/libyaml/yaml.go
|
||||
- internal/libyaml/yamlprivate.go
|
||||
|
||||
Copyright 2006-2011 - Kirill Simonov
|
||||
Copyright 2006-2010 Kirill Simonov
|
||||
https://opensource.org/license/mit
|
||||
|
||||
All the remaining project files are covered by the Apache license:
|
||||
|
||||
Copyright 2011-2019 - Canonical Ltd
|
||||
Copyright 2025 - The go-yaml Project Contributors
|
||||
Copyright 2011-2019 Canonical Ltd
|
||||
Copyright 2025 The go-yaml Project Contributors
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
golang.org/x/crypto
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
golang.org/x/sys
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
@@ -848,6 +999,40 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
gopkg.in/tomb.v1
|
||||
|
||||
tomb - support for clean goroutine termination in Go.
|
||||
|
||||
Copyright (c) 2010-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
github.com/golang/go
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Kor-Elf Shield
|
||||
### <strong>Русский</strong> <a href="README_ENG.md">English</a>
|
||||
<p style="color: red; font-weight: bold">ВНИМАНИЕ: Программа на стадии активной разработки и на данный момент NOT PRODUCTION READY</p>
|
||||
<p>Я больше 10-ти лет использовал ConfigServer Security and Firewall (csf) для защиты своего сервера. Но, к сожалению, в сентябре узнал, что компания, которая поддерживала этот великолепный продукт, закрылась 31 августа 2025 года. CSF написан на языке PERL. И компания выложила все исходные коды в свой репозиторий под лицензией GPLv3. Но я не знаю язык PERL. И мне его тяжело читать. :)</p>
|
||||
<p>Я решил реализовать своё решение на языке Go Lang. Это не будет полная копия CSF. Просто CSF вдохновило меня сделать, что-то похожее для защиты своего сервера.</p>
|
||||
|
||||
@@ -12,6 +11,7 @@
|
||||
* Linux 5.2+
|
||||
* nftables
|
||||
* Systemd
|
||||
* journalctl
|
||||
|
||||
***
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
* Подружить с docker (частично).
|
||||
* Внедрить настройку уведомлений (пока только e-mail).
|
||||
* Отправлять уведомления при авторизации ssh.
|
||||
* Защита от перебора паролей (brute-force).
|
||||
|
||||
### В планах:
|
||||
* Защита от перебора паролей (brute-force).
|
||||
* Уведомлять, если появится новый пользователь в системе.
|
||||
* Уведомлять, если изменились системные файлы.
|
||||
***
|
||||
@@ -61,6 +61,16 @@
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/firewall.toml</strong> - тут находятся настройки, связанные с nftables. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/firewall.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/firewall.toml</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/analyzer.toml</strong> - тут находятся настройки для анализатора логов. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/analyzer.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/analyzer.toml</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/notifications.toml</strong> - тут находятся настройки для уведомлений. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/notifications.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/notifications.toml</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/docker.toml</strong> - тут находятся настройки, связанные с docker. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/docker.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/docker.toml</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/blocklists.toml</strong> - тут находятся настройки для получения IP-адресов от разных сервисов, которые надо заблокировать. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/blocklists.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/blocklists.toml</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/geoip.toml</strong> - тут находятся настройки для получения данных об IP-адресах. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/geoip.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/geoip.toml</a></p>
|
||||
|
||||
***
|
||||
|
||||
<p>Программное обеспечение является MIT (см. <a href="https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/branch/main/LICENSE">LICENSE</a>) и использует сторонние библиотеки, которые распространяются на их собственных условиях (см. <a href="https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/branch/main/LICENSE-3RD-PARTY.txt">LICENSE-3RD-PARTY.txt</a>).</p>
|
||||
+12
-2
@@ -1,6 +1,5 @@
|
||||
# Kor-Elf Shield
|
||||
### <strong>English</strong> <a href="README.md">Русский</a>
|
||||
<p style="color: red; font-weight: bold">ATTENTION: The program is under active development and is NOT CURRENTLY PRODUCTION READY</p>
|
||||
<p>I have been using ConfigServer Security and Firewall (csf) for over 10 years to protect my server. But unfortunately, in September I found out that the company that supported this great product closed on August 31, 2025. CSF is written in PERL. And the company uploaded all the source codes to its repository under the GPLv3 license. But I do not know the PERL language. And it's hard for me to read it. :)</p>
|
||||
<p>I decided to implement my solution in the Go Lang language. It will not be a complete copy of CSF. CSF just inspired me to do something similar to protect my server.</p>
|
||||
|
||||
@@ -12,6 +11,7 @@
|
||||
* Linux 5.2+
|
||||
* nftables
|
||||
* Systemd
|
||||
* journalctl
|
||||
|
||||
***
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
* Make friends with docker (partially).
|
||||
* Implement notification settings (for now only by e-mail).
|
||||
* Send notifications during ssh authorization.
|
||||
* Password brute-force protection.
|
||||
|
||||
### The plans include:
|
||||
* Password brute-force protection.
|
||||
* Notify if a new user appears in the system.
|
||||
* Notify if system files have changed.
|
||||
***
|
||||
@@ -61,6 +61,16 @@
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/firewall.toml</strong> - Here are the settings related to nftables. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/firewall.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/firewall.toml/language/en</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/analyzer.toml</strong> - Contains log analyzer settings. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/analyzer.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/analyzer.toml/language/en</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/notifications.toml</strong> - Contains notification settings. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/notifications.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/notifications.toml/language/en</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/docker.toml</strong> - Contains Docker-related settings. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/docker.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/docker.toml/language/en</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/blocklists.toml</strong> - Contains settings for obtaining IP addresses from various services that need to be blocked. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/blocklists.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/blocklists.toml/language/en</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/geoip.toml</strong> - This contains settings for obtaining IP address data. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/geoip.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/geoip.toml/language/en</a></p>
|
||||
|
||||
***
|
||||
|
||||
<p>The software is MIT (see <a href="https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/branch/main/LICENSE">LICENSE</a>) and uses third-party libraries that are distributed on their own terms (see <a href="https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/branch/main/LICENSE-3RD-PARTY.txt">LICENSE-3RD-PARTY.txt</a>).</p>
|
||||
@@ -1,3 +1,261 @@
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Защита от перебора пароля
|
||||
# ***
|
||||
# SECTION:Protection against password brute-force attacks
|
||||
###############################################################################
|
||||
[bruteForceProtection]
|
||||
###
|
||||
# Включает группу отслеживания перебора пароля.
|
||||
# Если отключено, отслеживание перебора пароля работать не будет.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables the password attack monitoring group.
|
||||
# If disabled, password attack monitoring will not work.
|
||||
# Default: true
|
||||
###
|
||||
enabled = true
|
||||
|
||||
###
|
||||
# Включает уведомления об блокировок.
|
||||
# Если отключено, они будут отображаться в логах только на уровне = "info".
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables notifications about blocks.
|
||||
# If disabled, they will only appear in the logs under level = "info".
|
||||
# Default: true
|
||||
###
|
||||
notify = true
|
||||
|
||||
###
|
||||
# Максимальное количество ошибок, после которого произойдёт блокировка.
|
||||
# По умолчанию: 5
|
||||
# ***
|
||||
# The maximum number of errors after which a blocking will occur.
|
||||
# Default: 5
|
||||
###
|
||||
rate_limit_count = 5
|
||||
|
||||
###
|
||||
# Насколько времени в секундах блокировать IP адрес.
|
||||
# Если указать 0, то будет на всегда заблокирован.
|
||||
# По умолчанию: 3600
|
||||
# ***
|
||||
# How long in seconds to block an IP address.
|
||||
# If you specify 0, it will be blocked forever.
|
||||
# Default: 3600
|
||||
###
|
||||
blocking_time = 3600
|
||||
|
||||
###
|
||||
# Установите временной интервал для отслеживания сбоев входа в систему в течение секунд.
|
||||
# По умолчанию: 3600
|
||||
# ***
|
||||
# Set the time interval to monitor login failures in seconds.
|
||||
# Default: 3600
|
||||
###
|
||||
rate_limit_period = 3600
|
||||
|
||||
###
|
||||
# Указываем в секундах, через какое время сбрасывать данные IP в группе _default если не было событий.
|
||||
# Если указать 0, то не будет сбрасывать.
|
||||
# По умолчанию: 86400
|
||||
# ***
|
||||
# Specify the number of seconds after which IP data in the _default group will be reset if there have been no events.
|
||||
# If you specify 0, the reset will not occur.
|
||||
# Default: 86400
|
||||
###
|
||||
rate_limit_reset_period = 86400
|
||||
|
||||
###
|
||||
# Включает защиту от перебора пароля от ssh.
|
||||
# !!! Примечание: По умолчанию SSH-сервер не регистрирует на неудачные попытки входа с использованием ключа.
|
||||
# Чтобы включить отслеживание неудачных попыток входа с использованием ключа, необходимо добавить параметр
|
||||
# "LogLevel VERBOSE" в настройки sshd_config !!!
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables protection against brute-force attacks against ssh.
|
||||
# !!! Note: By default, the SSH server does not log unsuccessful login attempts using a key.
|
||||
# To enable tracking of unsuccessful login attempts using a key, you must add the "LogLevel VERBOSE" parameter
|
||||
# to the sshd_config settings !!!
|
||||
# Default: true
|
||||
###
|
||||
ssh_enable = true
|
||||
|
||||
###
|
||||
# Включает уведомления об блокировках, когда срабатывает защита от перебора пароля.
|
||||
# Если отключено, они будут отображаться в логах только на уровне = "info".
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables block notifications when password brute-force protection is triggered.
|
||||
# If disabled, they will only appear in the logs under level = "info".
|
||||
# Default: true
|
||||
###
|
||||
ssh_notify = true
|
||||
|
||||
###
|
||||
# Минимальный интервал в секундах между уведомлениями об успешной SSH-блокировке.
|
||||
# После отправки уведомления повторные уведомления не будут отправляться в течение указанного времени.
|
||||
# Если указать 0, ограничение по времени отключено.
|
||||
# По умолчанию: 0
|
||||
# ***
|
||||
# Minimum interval in seconds between notifications about successful SSH blocks.
|
||||
# After a notification is sent, repeated notifications will not be sent for the specified time.
|
||||
# If you specify 0, the time limit is disabled.
|
||||
# Default: 0
|
||||
###
|
||||
ssh_notify_cooldown_seconds = 0
|
||||
|
||||
###
|
||||
# Количество успешных SSH-блокировок, которое нужно пропустить после отправки уведомления.
|
||||
# Следующее уведомление будет отправлено после указанного количества новых сообщений о блокировке.
|
||||
# Этот параметр имеет приоритет над ssh_notify_cooldown_seconds.
|
||||
# Если указать 0, ограничение по количеству сообщений отключено.
|
||||
# По умолчанию: 0
|
||||
# ***
|
||||
# Number of successful SSH block messages to skip after sending a notification.
|
||||
# The next notification will be sent after the specified number of new block messages.
|
||||
# This parameter has priority over ssh_notify_cooldown_seconds.
|
||||
# If you specify 0, the message-count limit is disabled.
|
||||
# Default: 0
|
||||
###
|
||||
ssh_notify_every = 0
|
||||
|
||||
###
|
||||
# Можно указать свою группу, чтобы связать с другими правилами.
|
||||
# По умолчанию: ""
|
||||
# ***
|
||||
# You can specify your own group to link it to other rules.
|
||||
# Default: ""
|
||||
###
|
||||
ssh_group = ""
|
||||
|
||||
###
|
||||
# Указываем настройки группы.
|
||||
# Примеры:
|
||||
# [[bruteForceProtection.groups]]
|
||||
# name = "my_name_group" # Имя группы. Разрешены символы "a-z, A-Z, -, _". Первый символ обязательно буква (обязательное поле)
|
||||
# message = "Любой текст группы" # Текст уведомления (обязательное поле)
|
||||
# rate_limit_reset_period = 86400 # Указываем в секундах, через какое время сбрасывать данные в групе, если не было событий. Если указать 0, то не будет сбрасывать.
|
||||
## block_type = "ip_port" # Указываем тип блокировки: ip, ip_port. Если ничего не укажите, будет указан тип ip.
|
||||
## ports = ["22/tcp", "22/udp"] # Если тип блокировки стоит ip_port, то нужно указать порты, которые будут заблокированы после обнаружения попытки перебора пароля.
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## Через сколько будет срабатывать блокировка. В данном случае в течение часа, если было 5 обнаружений, то сработает блокировка.
|
||||
## И заблокирует на 10 минут.
|
||||
# count = 5
|
||||
# period = 3600
|
||||
# blocking_time = 600
|
||||
## Внутри bruteForceProtection.groups.rate_limits можно переопределить настройки block_type и ports.
|
||||
## block_type = "ip_port" # Указываем тип блокировки: ip, ip_port. Если ничего не укажите, будет указан тип ip.
|
||||
## ports = ["22/tcp", "22/udp", "80/tcp", "443/tcp"] # Если тип блокировки стоит ip_port, то нужно указать порты, которые будут заблокированы после обнаружения попытки перебора пароля.
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## После срабатывания блокировки, переходим на второй уровень, тепер если в течение часа было 3 обнаружений, то сработает блокировка.
|
||||
## И теперь заблокирует на час.
|
||||
# count = 3
|
||||
# period = 3600
|
||||
# blocking_time = 3600
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## И таких уровней можно указыват сколько захотите.
|
||||
# count = 2
|
||||
# period = 600
|
||||
# blocking_time = 3600
|
||||
#
|
||||
# ***
|
||||
# Specify group settings.
|
||||
# Examples:
|
||||
# [[bruteForceProtection.groups]]
|
||||
# name = "my_name_group" # Group name. Allowed characters are "a-z, A-Z, -, _". The first character must be a letter (required)
|
||||
# message = "Any group text" # Notification text (required)
|
||||
# rate_limit_reset_period = 86400 # Specify, in seconds, how long to reset group data if there have been no events. Specifying 0 means no reset.
|
||||
## block_type = "ip_port" # Specify the blocking type: IP, IP_port. If you don't specify anything, the IP type will be used.
|
||||
## ports = ["22/tcp", "22/udp"] # If the blocking type is ip_port, then you need to specify the ports that will be blocked after detecting a password brute-force attempt.
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## How long will it take for the block to be triggered? In this case, if there were 5 detections within an hour, the block will be triggered.
|
||||
## And it will block for 10 minutes.
|
||||
# count = 5
|
||||
# period = 3600
|
||||
# blocking_time = 600
|
||||
## Inside bruteForceProtection.groups.rate_limits you can override the block_type and ports settings.
|
||||
## block_type = "ip_port" # Specify the blocking type: IP, IP_port. If you don't specify anything, the IP type will be used.
|
||||
## ports = ["22/tcp", "22/udp", "80/tcp", "443/tcp"] # If the blocking type is ip_port, then you need to specify the ports that will be blocked after detecting a password brute-force attempt.
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## After the blocking is triggered, we move to the second level. Now, if there are three detections within an hour, the blocking will be triggered.
|
||||
## And now it will block for an hour.
|
||||
# count = 3
|
||||
# period = 3600
|
||||
# blocking_time = 3600
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## You can specify as many of these levels as you like.
|
||||
# count = 2
|
||||
# period = 600
|
||||
# blocking_time = 3600
|
||||
###
|
||||
|
||||
###
|
||||
# Указываем настройки логов, которые надо отслеживать для защиты от перебора пароля.
|
||||
# Примеры:
|
||||
# [[bruteForceProtection.rules]]
|
||||
# enabled = true # Включает или выключает отслеживания (обязательное поле)
|
||||
# notify = true # Включает или выключает уведомления (обязательное поле)
|
||||
# notify_cooldown_seconds = 0 # Минимальный интервал в секундах между уведомлениями об успешной блокировке (не обязательное поле)
|
||||
# notify_every = 0 # Количество успешных блокировок, которое нужно пропустить после отправки уведомления (не обязательное поле)
|
||||
# name = "my_name_rule" # Имя уведомления. Разрешены символы "a-z, A-Z, -, _". Первый символ обязательно буква (обязательное поле)
|
||||
# message = "Ваш любой текст для уведомления" # Текст уведомления (обязательное поле)
|
||||
# group = "my_name_group" # Можно указать имя группы (не обязательное поле)
|
||||
# [bruteForceProtection.rules.source]
|
||||
# type = "journalctl" # journalctl или file (обязательное поле)
|
||||
# path = "" # Указываем путь к файлу (обязательное поле если type = "file")
|
||||
# field = "systemd_unit" # systemd_unit или syslog_identifier (обязательное поле если type = "journalctl")
|
||||
# match = "ssh.service" # Значение (обязательное поле если type = "journalctl")
|
||||
# если field = "systemd_unit", то match должен заканичваться: .service, .socket, .target, .mount, .timer, .path, .scope, .slice, .device
|
||||
# [[bruteForceProtection.rules.patterns]]
|
||||
# regexp = '^Failed password for (\S+) from (\S+) port \S+'
|
||||
# ip = 2 # Указываем номер value, который укажет IP (обязательное поле)
|
||||
# [[bruteForceProtection.rules.patterns.values]]
|
||||
# name = "Пользователь"
|
||||
# value = 1
|
||||
# [bruteForceProtection.rules.patterns.partition] # (необязательный параметр) если надо разделить по ключевым словам
|
||||
# value = 1 # Номер поля, по которому мы получаем ключевое слова
|
||||
# trim = true # Убирает у ключевого слова пробелы вначале и в конце
|
||||
# lower_case = true # Ключевое слово приводит к нижнему регистру
|
||||
# [bruteForceProtection.rules.patterns.partition.type] # (необязательный параметр)
|
||||
# type = "only" # Допустимые значения "only" или "except"
|
||||
# keywords = ["root", "user"] # Указываем ключевые слова
|
||||
# partitioned = false # Если нужно, чтобы каждое ключевое слово считалось независимо от друг-друга, то указываем true
|
||||
# ***
|
||||
# Specify the log settings that need to be monitored to protect against password brute-force attacks.
|
||||
# Examples:
|
||||
# [[bruteForceProtection.rules]]
|
||||
# enabled = true # Enables or disables tracking (required)
|
||||
# notify = true # Enables or disables notifications (required)
|
||||
# notify_cooldown_seconds = 0 # Minimum interval in seconds between notifications about successful blocks (optional field)
|
||||
# notify_every = 0 # Number of successful block messages to skip after sending a notification (optional field)
|
||||
# name = "my_name_rule" # Notification name. Allowed characters are "a-z, A-Z, -, _". The first character must be a letter (required field)
|
||||
# message = "Your any text for notification" # Notification text (required field)
|
||||
# group = "my_name_group" # You can specify the group name (optional field)
|
||||
# [bruteForceProtection.rules.source]
|
||||
# type = "journalctl" # journalctl or file (required)
|
||||
# path = "" # Specify the path to the file (required field if type = "file")
|
||||
# field = "systemd_unit" # systemd_unit or syslog_identifier (required if type = "journalctl")
|
||||
# match = "ssh.service" # Value (required if type = "journalctl")
|
||||
# If field = "systemd_unit", then match must end with: .service, .socket, .target, .mount, .timer, .path, .scope, .slice, .device
|
||||
# [[bruteForceProtection.rules.patterns]]
|
||||
# regexp = '^Accepted (\S+) for (\S+) from (\S+) port \S+'
|
||||
# [[bruteForceProtection.rules.patterns]]
|
||||
# regexp = '^Failed password for (\S+) from (\S+) port \S+'
|
||||
# ip = 2 # We indicate the value number that will indicate the IP (required field)
|
||||
# [[bruteForceProtection.rules.patterns.values]]
|
||||
# name = "User"
|
||||
# value = 1
|
||||
# [bruteForceProtection.rules.patterns.partition] # (optional) if you want to split by keywords
|
||||
# value = 1 # Field number by which to get the keyword
|
||||
# trim = true # Removes leading and trailing spaces from the keyword
|
||||
# lower_case = true # Converts the keyword to lowercase
|
||||
# [bruteForceProtection.rules.patterns.partition.type] # (optional)
|
||||
# type = "only" # Acceptable values are "only" or "except"
|
||||
# keywords = ["root", "user"] # Specify the keywords
|
||||
# partitioned = false # If you want each keyword to be considered independently, specify true
|
||||
###
|
||||
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Отслеживать авторизаций
|
||||
# ***
|
||||
@@ -108,3 +366,138 @@ sudo_enable = false
|
||||
###
|
||||
sudo_notify = true
|
||||
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Настройки анализа логов для уведомления
|
||||
# ***
|
||||
# SECTION:Log analysis settings for notifications
|
||||
###############################################################################
|
||||
[logAlert]
|
||||
###
|
||||
# Включает группу отслеживания логов для оповещения.
|
||||
# Если отключено, отслеживание логов для оповещения работать не будет.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables the log monitoring group for alerts.
|
||||
# If disabled, log monitoring for alerts will not work.
|
||||
# Default: true
|
||||
###
|
||||
enabled = true
|
||||
|
||||
###
|
||||
# Включает уведомления.
|
||||
# Если отключено, они будут отображаться в логах только на уровне = "info".
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables notifications.
|
||||
# If disabled, they will only appear in the logs under level = "info".
|
||||
# Default: true
|
||||
###
|
||||
notify = true
|
||||
|
||||
###
|
||||
# Указываем настройки группы.
|
||||
# Примеры:
|
||||
# [[logAlert.groups]]
|
||||
# name = "my_name_group" # Имя группы. Разрешены символы "a-z, A-Z, -, _". Первый символ обязательно буква (обязательное поле)
|
||||
# message = "Любой текст группы" # Текст уведомления (обязательное поле)
|
||||
# rate_limit_reset_period = 86400 # Указываем в секундах, через какое время сбрасывать данные в групе если не было событий. Если указать 0, то не будет сбрасывать.
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## Через сколько будет срабатывать оповещение. В данном случае в течение часа, если было 5 обнаружений, то сработает оповещение.
|
||||
# count = 5
|
||||
# period = 3600
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## После срабатывания оповещения, переходим на второй уровень, тепер если в течение часа было 3 обнаружений, то сработает оповещение.
|
||||
# count = 3
|
||||
# period = 3600
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## И таких уровней можно указыват сколько захотите.
|
||||
# count = 2
|
||||
# period = 600
|
||||
#
|
||||
# ***
|
||||
# Specify group settings.
|
||||
# Examples:
|
||||
# [[logAlert.groups]]
|
||||
# name = "my_name_group" # Group name. Allowed characters are "a-z, A-Z, -, _". The first character must be a letter (required)
|
||||
# message = "Any group text" # Notification text (required)
|
||||
# rate_limit_reset_period = 86400 # Specify, in seconds, how long to reset group data if there have been no events. Specifying 0 means no reset.
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## How long to wait before an alert is triggered. In this case, if there were 5 detections within an hour, the alert will be triggered. # count = 5
|
||||
# count = 5
|
||||
# period = 3600
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## After the alert is triggered, we move to the second level. Now, if there are 3 detections within an hour, the alert will be triggered.
|
||||
# count = 3
|
||||
# period = 3600
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## You can specify as many of these levels as you like.
|
||||
# count = 2
|
||||
# period = 600
|
||||
###
|
||||
|
||||
###
|
||||
# Указываем настройки логов, которые надо отслеживать для оповещения.
|
||||
# Примеры:
|
||||
# [[logAlert.rules]]
|
||||
# enabled = true # Включает или выключает отслеживания (обязательное поле)
|
||||
# notify = true # Включает или выключает уведомления (обязательное поле)
|
||||
# name = "my_name_rule" # Имя уведомления. Разрешены символы "a-z, A-Z, -, _". Первый символ обязательно буква (обязательное поле)
|
||||
# message = "Ваш любой текст для уведомления" # Текст уведомления (обязательное поле)
|
||||
# group = "my_name_group" # Можно указать имя группы (не обязательное поле)
|
||||
# [logAlert.rules.source]
|
||||
# type = "journalctl" # journalctl или file (обязательное поле)
|
||||
# path = "" # Указываем путь к файлу (обязательное поле если type = "file")
|
||||
# field = "systemd_unit" # systemd_unit или syslog_identifier (обязательное поле если type = "journalctl")
|
||||
# match = "ssh.service" # Значение (обязательное поле если type = "journalctl")
|
||||
# если field = "systemd_unit", то match должен заканичваться: .service, .socket, .target, .mount, .timer, .path, .scope, .slice, .device
|
||||
# [[logAlert.rules.patterns]]
|
||||
# regexp = '^Accepted (\S+) for (\S+) from (\S+) port \S+'
|
||||
# [[logAlert.rules.patterns.values]]
|
||||
# name = "Пользователь"
|
||||
# value = 2
|
||||
# [[logAlert.rules.patterns.values]]
|
||||
# name = "IP"
|
||||
# value = 3
|
||||
# type = "ip"
|
||||
# [logAlert.rules.patterns.partition] # (необязательный параметр) если надо разделить по ключевым словам
|
||||
# value = 2 # Номер поля, по которому мы получаем ключевое слова
|
||||
# trim = true # Убирает у ключевого слова пробелы вначале и в конце
|
||||
# lower_case = true # Ключевое слово приводит к нижнему регистру
|
||||
# [logAlert.rules.patterns.partition.type] # (необязательный параметр)
|
||||
# type = "only" # Допустимые значения "only" или "except"
|
||||
# keywords = ["root", "user"] # Указываем ключевые слова
|
||||
# partitioned = false # Если нужно, чтобы каждое ключевое слово считалось независимо от друг-друга, то указываем true
|
||||
#
|
||||
# ***
|
||||
# Specify the log settings to monitor for notifications.
|
||||
# Examples:
|
||||
# [[logAlert.rules]]
|
||||
# enabled = true # Enables or disables tracking (required)
|
||||
# notify = true # Enables or disables notifications (required)
|
||||
# name = "my_name_rule" # Notification name. Allowed characters are "a-z, A-Z, -, _". The first character must be a letter (required field)
|
||||
# message = "Your any text for notification" # Notification text (required field)
|
||||
# group = "my_name_group" # You can specify the group name (optional field)
|
||||
# [logAlert.rules.source]
|
||||
# type = "journalctl" # journalctl or file (required)
|
||||
# path = "" # Specify the path to the file (required field if type = "file")
|
||||
# field = "systemd_unit" # systemd_unit or syslog_identifier (required if type = "journalctl")
|
||||
# match = "ssh.service" # Value (required if type = "journalctl")
|
||||
# If field = "systemd_unit", then match must end with: .service, .socket, .target, .mount, .timer, .path, .scope, .slice, .device
|
||||
# [[logAlert.rules.patterns]]
|
||||
# regexp = '^Accepted (\S+) for (\S+) from (\S+) port \S+'
|
||||
# [[logAlert.rules.patterns.values]]
|
||||
# name = "User"
|
||||
# value = 2
|
||||
# [[logAlert.rules.patterns.values]]
|
||||
# name = "IP"
|
||||
# value = 3
|
||||
# type = "ip"
|
||||
# [logAlert.rules.patterns.partition] # (optional) if you want to split by keywords
|
||||
# value = 2 # Field number by which we get the keyword
|
||||
# trim = true # Removes leading and trailing spaces from the keyword
|
||||
# lower_case = true # Converts the keyword to lowercase
|
||||
# [logAlert.rules.patterns.partition.type] # (optional)
|
||||
# type = "only" # Acceptable values are "only" or "except"
|
||||
# keywords = ["root", "user"] # Specify the keywords
|
||||
# partitioned = false # If you want each keyword to be counted independently, specify true
|
||||
###
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Настройки для определения списков блокировки IP-адресов
|
||||
# ***
|
||||
# SECTION:Settings for defining IP address block lists
|
||||
###############################################################################
|
||||
|
||||
###
|
||||
# Обеспечивает поддержку получения списков заблокированных IP-адресов.
|
||||
# !!! Для полученние данных нужно открыть исходящий порт 443. !!!
|
||||
# !!! После включения не забудьте включить необходимые списки, так как по умолчанию они все отключены. !!!
|
||||
# По умолчанию: false
|
||||
# ***
|
||||
# Enables support for retrieving IP block lists.
|
||||
# !!! To receive data, you need to open outgoing port 443. !!!
|
||||
# !!! After turning on, do not forget to turn on the necessary lists, as they are all turned off by default. !!!
|
||||
# Default: false
|
||||
###
|
||||
enabled = false
|
||||
|
||||
###
|
||||
# Исключить IP-адреса из полученных списков.
|
||||
# Допустимые форматы: "192.168.1.1", "192.168.1.0/24", "192.168.1.0-192.168.1.255"
|
||||
# По умолчанию: [
|
||||
# "127.0.0.1/8",
|
||||
# "10.0.0.0/8",
|
||||
# "172.16.0.0/12",
|
||||
# "192.168.0.0/16",
|
||||
# "::1/128",
|
||||
# "fc00::/7",
|
||||
# ]
|
||||
# ***
|
||||
# Exclude IP addresses from retrieved lists.
|
||||
# Acceptable formats: "192.168.1.1", "192.168.1.0/24", "192.168.1.0-192.168.1.255"
|
||||
# Default: [
|
||||
# "127.0.0.1/8",
|
||||
# "10.0.0.0/8",
|
||||
# "172.16.0.0/12",
|
||||
# "192.168.0.0/16",
|
||||
# "::1/128",
|
||||
# "fc00::/7",
|
||||
# ]
|
||||
###
|
||||
exclude_ips = [
|
||||
"127.0.0.1/8",
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"::1/128",
|
||||
"fc00::/7",
|
||||
]
|
||||
|
||||
###
|
||||
# Spamhaus Don't Route Or Peer Lists (DROP IPv4)
|
||||
# Details: https://www.spamhaus.org/blocklists/do-not-route-or-peer/
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "SPAMDROP"
|
||||
url = "https://www.spamhaus.org/drop/drop_v4.json"
|
||||
format = "json"
|
||||
json_field = "cidr"
|
||||
limit = 0
|
||||
interval = 86400
|
||||
|
||||
###
|
||||
# Spamhaus Don't Route Or Peer Lists (DROP IPv6)
|
||||
# Details: https://www.spamhaus.org/blocklists/do-not-route-or-peer/
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "SPAMDROPV6"
|
||||
url = "https://www.spamhaus.org/drop/drop_v6.json"
|
||||
format = "json"
|
||||
json_field = "cidr"
|
||||
limit = 0
|
||||
interval = 86400
|
||||
|
||||
###
|
||||
# DShield.org Recommended Block List
|
||||
# Details: https://dshield.org
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "DSHIELD"
|
||||
url = "https://www.dshield.org/block.txt"
|
||||
format = "txt"
|
||||
txt_type = "cidr"
|
||||
txt_field_ip = 0
|
||||
txt_field_cidr = 2
|
||||
txt_separator = "\t"
|
||||
limit = 0
|
||||
interval = 86400
|
||||
###
|
||||
# Тот же список, только мы получаем данные не через CIDR, а через диапазон IP-адресов.
|
||||
# !!! Не рекомендуется включать DSHIELD и DSHIELD_INTERVAL одновременно. !!!
|
||||
# ***
|
||||
# The same list, only we receive data not via CIDR, but via a range of IP addresses.
|
||||
# !!! It is not recommended to turn on DSHIELD and DSHIELD_INTERVAL together. !!!
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "DSHIELD_INTERVAL"
|
||||
url = "https://www.dshield.org/block.txt"
|
||||
format = "txt"
|
||||
txt_type = "interval"
|
||||
txt_field_ip = 0
|
||||
txt_field_ip2 = 1
|
||||
txt_separator = "\t"
|
||||
limit = 0
|
||||
interval = 600
|
||||
|
||||
###
|
||||
# TOR Exit Nodes List
|
||||
# Details: https://trac.torproject.org/projects/tor/wiki/doc/TorDNSExitList
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "TOR"
|
||||
url = "https://check.torproject.org/torbulkexitlist"
|
||||
format = "txt"
|
||||
txt_type = "default"
|
||||
txt_field_ip = 0
|
||||
txt_separator = " "
|
||||
limit = 0
|
||||
interval = 86400
|
||||
|
||||
###
|
||||
# Project Honey Pot Directory of Dictionary Attacker IPs
|
||||
# Details: https://www.projecthoneypot.org/
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "HONEYPOT"
|
||||
url = "https://www.projecthoneypot.org/list_of_ips.php?t=d&rss=1"
|
||||
format = "rss"
|
||||
rss_tag = "item"
|
||||
rss_field = "title"
|
||||
rss_field_ip = 0
|
||||
rss_field_separator = "|"
|
||||
limit = 0
|
||||
interval = 86400
|
||||
|
||||
###
|
||||
# C.I. Army Malicious IP List
|
||||
# Details: https://www.ciarmy.com/
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "CIARMY"
|
||||
url = "https://www.ciarmy.com/list/ci-badguys.txt"
|
||||
format = "txt"
|
||||
txt_type = "default"
|
||||
txt_field_ip = 0
|
||||
txt_separator = " "
|
||||
limit = 0
|
||||
interval = 86400
|
||||
|
||||
###
|
||||
# BruteForceBlocker IP List
|
||||
# Details: https://danger.rulez.sk/index.php/bruteforceblocker/
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "BFB"
|
||||
url = "https://danger.rulez.sk/projects/bruteforceblocker/blist.php"
|
||||
format = "txt"
|
||||
txt_type = "default"
|
||||
txt_field_ip = 0
|
||||
txt_separator = "\t"
|
||||
limit = 0
|
||||
interval = 86400
|
||||
|
||||
###
|
||||
# Blocklist.de
|
||||
# Details: https://www.blocklist.de
|
||||
# В первом списке отображаются только IP-адреса, добавленные за последний час.
|
||||
# ***
|
||||
# This first list only retrieves the IP addresses added in the last hour.
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "BDE"
|
||||
url = "https://api.blocklist.de/getlast.php?time=3600"
|
||||
format = "txt"
|
||||
txt_type = "default"
|
||||
txt_field_ip = 0
|
||||
txt_separator = "\t"
|
||||
limit = 0
|
||||
interval = 86400
|
||||
###
|
||||
# Второй список содержит все IP-адреса, добавленные за последние 48 часов.
|
||||
# Обычно это очень большой список (более 10000 записей), поэтому убедитесь, что у вас
|
||||
# есть необходимые ресурсы для его использования.
|
||||
# !!! Не рекомендуется включать BDE и BDEALL одновременно. !!!
|
||||
# ***
|
||||
# This second list retrieves all the IP addresses added in the last 48 hours
|
||||
# and is usually a very large list (over 10000 entries), so be sure that you
|
||||
# have the resources available to use it
|
||||
# !!! It is not recommended to turn on BDE and BDEALL together. !!!
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "BDEALL"
|
||||
url = "https://lists.blocklist.de/lists/all.txt"
|
||||
format = "txt"
|
||||
txt_type = "default"
|
||||
txt_field_ip = 0
|
||||
txt_separator = "\t"
|
||||
limit = 0
|
||||
interval = 86400
|
||||
|
||||
# Stop Forum Spam (IPv4)
|
||||
# Details: https://www.stopforumspam.com/downloads
|
||||
# Многие из доступных списков содержат огромное количество IP-адресов,
|
||||
# поэтому следует проявлять осторожность при выборе из этих списков.
|
||||
# ***
|
||||
# Many of the lists available contain a vast number of IP addresses so special
|
||||
# care needs to be made when selecting from their lists.
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "STOPFORUMSPAM"
|
||||
url = "https://www.stopforumspam.com/downloads/listed_ip_1.zip"
|
||||
zip = true
|
||||
format = "txt"
|
||||
txt_type = "default"
|
||||
txt_field_ip = 0
|
||||
txt_separator = " "
|
||||
limit = 0
|
||||
interval = 86400
|
||||
|
||||
# Stop Forum Spam (IPv6)
|
||||
# Details: https://www.stopforumspam.com/downloads
|
||||
# Многие из доступных списков содержат огромное количество IP-адресов,
|
||||
# поэтому следует проявлять осторожность при выборе из этих списков.
|
||||
# ***
|
||||
# Many of the lists available contain a vast number of IP addresses so special
|
||||
# care needs to be made when selecting from their lists.
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "STOPFORUMSPAMV6"
|
||||
url = "https://www.stopforumspam.com/downloads/listed_ip_1_ipv6.zip"
|
||||
zip = true
|
||||
format = "txt"
|
||||
txt_type = "default"
|
||||
txt_field_ip = 0
|
||||
txt_separator = " "
|
||||
limit = 0
|
||||
interval = 86400
|
||||
|
||||
###
|
||||
# GreenSnow Hack List
|
||||
# Details: https://greensnow.co
|
||||
###
|
||||
[[sources]]
|
||||
enabled = false
|
||||
name = "GREENSNOW"
|
||||
url = "https://blocklist.greensnow.co/greensnow.txt"
|
||||
format = "txt"
|
||||
txt_type = "default"
|
||||
txt_field_ip = 0
|
||||
txt_separator = " "
|
||||
limit = 0
|
||||
interval = 86400
|
||||
|
||||
###############################################################################
|
||||
# Если вы хотите реализовать свой собственный список или хотите понять параметры,
|
||||
# описание параметров вам может помочь:
|
||||
# ***
|
||||
# If you want to implement your own list or want to understand the parameters,
|
||||
# the parameter description might help:
|
||||
###############################################################################
|
||||
#
|
||||
# [[sources]]
|
||||
#
|
||||
# Включает или выключает получения данных с этого списка
|
||||
# ***
|
||||
# Enables or disables retrieving data from this list
|
||||
# enabled = false
|
||||
#
|
||||
# Имя, которое будет использоваться в создание set в nftables.
|
||||
# Имя должно быть уникальное и разрешены символы "a-z, A-Z, -, _"
|
||||
# ***
|
||||
# The name that will be used when creating a set in nftables.
|
||||
# The name must be unique and the characters "a-z, A-Z, -, _" are allowed.
|
||||
# name = "SPAMDROP"
|
||||
#
|
||||
# Адрес по которому будет программа обращаться, чтобы получить список IP адресов для блокировки.
|
||||
# ***
|
||||
# The address that the program will contact to obtain a list of IP addresses to block.
|
||||
# url = "https://www.spamhaus.org/drop/drop_v4.json"
|
||||
#
|
||||
# Это максимальное количество IP-адресов из списка, которые можно использовать.
|
||||
# Значение 0 означает использование всех IP-адресов.
|
||||
# ***
|
||||
# This is the maximum number of IP addresses from the list that can be used.
|
||||
# A value of 0 means all IP addresses are used.
|
||||
# limit = 0
|
||||
#
|
||||
# Интервал обновления для загрузки списка должен составлять минимум 60 секунд (не рекомендуется),
|
||||
# но 86400 (в день) будет более чем достаточно.
|
||||
# ***
|
||||
# The refresh interval for loading the list should be at least 60 seconds (not recommended),
|
||||
# but 86400 (per day) will be more than enough.
|
||||
# interval = 86400
|
||||
#
|
||||
# Если получаемый файл в zip формате, то надо включить zip = true.
|
||||
# ***
|
||||
# If the file received is in zip format, then you must include zip = true.
|
||||
# zip = false
|
||||
#
|
||||
# Есть несколько форматов: "json", "txt", "rss".
|
||||
# ***
|
||||
# There are several formats: "json", "txt", "rss".
|
||||
# format = "json"
|
||||
#
|
||||
# Это поле используется, если `format = "json"`. Здесь мы указываем имя ключа, по которому получаем IP-адрес.
|
||||
# ***
|
||||
# This field is used if the `format = "json"`. Here we specify the name of the key by which we obtain the IP address.
|
||||
# json_field = "cidr"
|
||||
#
|
||||
# Это поле используется, если `format = "txt"`. Допустимые значения: "default", "cidr", "interval"
|
||||
# ***
|
||||
# This field is used if `format = "txt"`. Valid values: "default", "cidr", "interval"
|
||||
# txt_type = "cidr"
|
||||
#
|
||||
# Это поле используется, если `format = "txt"`. Это индекс поля для получения IP адреса.
|
||||
# Допустимые значения от 0 и выше.
|
||||
# ***
|
||||
# This field is used if `format = "txt"`. This is the field index for obtaining the IP address.
|
||||
# Valid values are 0 and higher.
|
||||
# txt_field_ip = 0
|
||||
#
|
||||
# Это поле используется, если `format = "txt"`. Это разделитель. Парсер использует его для разделения строки на поля.
|
||||
# ***
|
||||
# This field is used if `format = "txt"`. It's a delimiter. The parser uses it to divide the string into fields.
|
||||
# txt_separator = "\t"
|
||||
#
|
||||
# Это поле используется, если `format = "txt"` и `txt_type = "cidr"`. Указывает индекс поля для получения cidr.
|
||||
# Допустимые значения от 0 и выше.
|
||||
# ***
|
||||
# This field is used if `format = "txt"` and `txt_type = "cidr"`. Specifies the field index to retrieve the cidr.
|
||||
# Valid values are 0 and above.
|
||||
# txt_field_cidr = 2
|
||||
#
|
||||
# Это поле используется, если `format = "txt"` и `txt_type = "interval"`.
|
||||
# Указывает индекс поля для получения второго IP (по). Допустимые значения от 0 и выше.
|
||||
# ***
|
||||
# This field is used if `format = "txt"` and `txt_type = "interval"`.
|
||||
# Specifies the field index to retrieve the second IP (to). Valid values are 0 and higher.
|
||||
# txt_field_ip2 = 1
|
||||
#
|
||||
# Это поле используется, если `format = "rss"`. Указывает на родительский тег.
|
||||
# ***
|
||||
# This field is used if `format="rss"`. Points to the parent tag.
|
||||
# rss_tag = "item"
|
||||
#
|
||||
# Это поле используется, если `format = "rss"`. Указывает на тег, который находится внутри родительского тега.
|
||||
# ***
|
||||
# This field is used if `format="rss"`. Points to a tag that is inside the parent tag.
|
||||
# rss_field = "title"
|
||||
#
|
||||
# Это поле используется, если `format = "rss"`. Если нужно разделить получаемый текст.
|
||||
# ***
|
||||
# This field is used if `format = "rss"`. If you need to split the received text.
|
||||
# rss_field_separator = "|"
|
||||
#
|
||||
# Это поле используется, если `format = "rss"` и указан `rss_field_separator`. Это индекс поля для получения IP адреса.
|
||||
# Допустимые значения от 0 и выше.
|
||||
# ***
|
||||
# This field is used if `format = "rss"` and `rss_field_separator` is specified. This is the field index for obtaining the IP address.
|
||||
# Valid values are 0 and above.
|
||||
# rss_field_ip = 0
|
||||
###
|
||||
@@ -300,6 +300,17 @@ icmp_strict = false
|
||||
###############################################################################
|
||||
[options]
|
||||
|
||||
###
|
||||
# Включает кэширование, чтобы избежать постоянной компиляции команд nftables во временный файл.
|
||||
# Файл кэша изменяется после изменения настроек или обновления версии программы.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables a cache to avoid constantly compiling nftables commands into a temporary file.
|
||||
# The cache file changes after changing settings or updating the program version.
|
||||
# Default: true
|
||||
###
|
||||
cache = true
|
||||
|
||||
###
|
||||
# Переключения режима очистки фаервола nftables. Если указать "own", то может получиться конфликт в правилах.
|
||||
# Может спровоцировать проблему в безопасности. Указывайте "own" если вы уверены в своих действиях.
|
||||
@@ -508,6 +519,67 @@ forward_drop = "drop"
|
||||
###
|
||||
forward_priority = -10
|
||||
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Port knocking
|
||||
# ***
|
||||
# SECTION:Port knocking
|
||||
###############################################################################
|
||||
###
|
||||
# Тут можно настрить Port knocking. Это когда надо открыть порт, только поcле определённых стуков по определённому портам.
|
||||
#
|
||||
# Пример:
|
||||
# [[portKnocking]]
|
||||
# name = "ssh" # Имя должно быть уникальное и разрешены символы: "a-z, A-Z, -, _"
|
||||
# port = 22 # Номер порта, который нужно открыть после всех стуков
|
||||
# protocol = "tcp" # Протокол: tcp, udp
|
||||
# ip_version = "ip4" # Версия IP: ip4, ip6
|
||||
# [[portKnocking.knock]] # Первый стук
|
||||
# port = 2222 # Порт стука
|
||||
# protocol = "tcp" # Протокол: tcp, udp
|
||||
# timeout = 30 # Время в секундах на которое работает стук
|
||||
# action = "drop" # Во время стука, какой ответ отдавать: accept, return, drop or reject
|
||||
## Лучше установить для параметра "action" значение "drop", чтобы любой, кто попытается угадать такие порты для "стука",
|
||||
## не смог отличить их от заблокированного порта.
|
||||
# [[portKnocking.knock]] # Второй стук (можно добавлять сколько хотите)
|
||||
# port = 2225
|
||||
# protocol = "tcp"
|
||||
# timeout = 30
|
||||
# action = "drop"
|
||||
#
|
||||
# Рассмотрим пример. Мы назвали настройку для portKnocking "ssh". В nftables будут созданны наборы sets таким образом:
|
||||
# knock_ssh_0, knock_ssh_1. Мы открываем 22/tcp порт для IP адреса, который прошёл в данном случае два стука.
|
||||
# 1 стук надо сделать на 2222/tcp и в течении 30 секунд надо сделать второй стук на порт 2225/tcp.
|
||||
# После второго стука будет открыт для данного IP адреса порт 22/tcp на 30 секунд.
|
||||
#
|
||||
# ***
|
||||
#
|
||||
# Here you can configure port knocking. This allows you to open a port only after certain knocks on a specific ports.
|
||||
#
|
||||
# Example:
|
||||
# [[portKnocking]]
|
||||
# name = "ssh" # The name must be unique and symbols are allowed: "a-z, A-Z, -, _"
|
||||
# port = 22 # The port number that needs to be opened after all the knocking
|
||||
# protocol = "tcp" # Protocol: tcp, udp
|
||||
# ip_version = "ip4" # IP version: ip4, ip6
|
||||
# [[portKnocking.knock]] # The first knock
|
||||
# port = 2222 # Port of knocking
|
||||
# protocol = "tcp" # Protocol: tcp, udp
|
||||
# timeout = 30 # The time in seconds for which the knocking works
|
||||
# action = "drop" # When knocking, what answer should I give: accept, return, drop or reject
|
||||
## It's best to set the "action" parameter to "drop" so that anyone trying to guess such ports for "knocking"
|
||||
## won't be able to distinguish them from a blocked port.
|
||||
# [[portKnocking.knock]] # Second knock (you can add as many as you want)
|
||||
# port = 2225
|
||||
# protocol = "tcp"
|
||||
# timeout = 30
|
||||
# action = "drop"
|
||||
#
|
||||
# Let's look at an example. We named the portKnocking setting "ssh." Sets will be created in nftables as follows:
|
||||
# knock_ssh_0, knock_ssh_1. We open port 22/tcp for the IP address that, in this case, has been knocked twice.
|
||||
# The first knock should be made on 2222/tcp, and within 30 seconds, a second knock should be made on port 2225/tcp.
|
||||
# After the second knock, port 22/tcp will be opened for this IP address for 30 seconds.
|
||||
###
|
||||
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Именование метаданных
|
||||
# ***
|
||||
@@ -549,3 +621,46 @@ chain_output_name = "output"
|
||||
# Default: "forward"
|
||||
###
|
||||
chain_forward_name = "forward"
|
||||
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Проверка и восстановление правил брандмауэра
|
||||
# ***
|
||||
# SECTION:Checking and restoring firewall rules
|
||||
###############################################################################
|
||||
[rulesGuard]
|
||||
###
|
||||
# Включает мониторинг правил NFTables.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables monitoring of NFTables rules.
|
||||
# Default: true
|
||||
###
|
||||
enabled = true
|
||||
|
||||
###
|
||||
# Включает уведомления в случае возникновения проблем с правилами NFTables.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables notifications if there is a problem with NFTables rules.
|
||||
# Default: true
|
||||
###
|
||||
notifications = true
|
||||
|
||||
###
|
||||
# Если возникла проблема с правилами NFTables, следует ли перезапустить настройки правил NFTables.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# If there is a problem with the NFTables rules, whether to restart the NFTables rules settings.
|
||||
# Default: true
|
||||
###
|
||||
recovery = true
|
||||
|
||||
###
|
||||
# Указать интервал в секундах по которому будет проверяться на наличия правил NFTables.
|
||||
# По умолчанию: 3600
|
||||
# ***
|
||||
# Specify the interval in seconds at which to check for NFTables rules.
|
||||
# Default: 3600
|
||||
###
|
||||
interval = 3600
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Настройки для получения данных об IP-адресах
|
||||
# ***
|
||||
# SECTION:Settings for obtaining IP address data
|
||||
###############################################################################
|
||||
|
||||
###
|
||||
# Включает или выключает получения данных об IP-адресах.
|
||||
# !!! Не забудьте перед включением настроить данные от сервиса maxmind.com !!!
|
||||
# false = Выключает.
|
||||
# true = Включает.
|
||||
#
|
||||
# По умолчанию: false
|
||||
# ***
|
||||
# Enables or disables retrieval of IP address data.
|
||||
# !!! Don't forget to configure the data from the maxmind.com service before turning it on !!!
|
||||
# false = Disables.
|
||||
# true = Enables.
|
||||
#
|
||||
# Default: false
|
||||
###
|
||||
enabled = false
|
||||
|
||||
###
|
||||
# Мы указываем, через какой сервис мы будем получать данные об IP-адресе.
|
||||
# По умолчанию: maxmind
|
||||
# ***
|
||||
# We indicate through which service we will receive data about the IP address.
|
||||
# Default: maxmind
|
||||
###
|
||||
service = "maxmind"
|
||||
|
||||
###############################################################################
|
||||
# СЕРВИС: MaxMind https://www.maxmind.com/en/geolite-free-ip-geolocation-data
|
||||
# ***
|
||||
# SERVICE: MaxMind https://www.maxmind.com/en/geolite-free-ip-geolocation-data
|
||||
###############################################################################
|
||||
[maxmind]
|
||||
|
||||
###
|
||||
# ID пользователя.
|
||||
# Чтобы получить, нужно вначале зарегестрироватья тут: https://www.maxmind.com/en/geolite2/signup
|
||||
# ***
|
||||
# User ID.
|
||||
# To receive it, you must first register here: https://www.maxmind.com/en/geolite2/signup
|
||||
###
|
||||
username = ""
|
||||
|
||||
###
|
||||
# Лицензионный ключ. Его надо сгенерировать в личном кабинете.
|
||||
# ***
|
||||
# License Key. It must be generated in your personal account.
|
||||
####
|
||||
password = ""
|
||||
|
||||
###
|
||||
# Интервал обновления для обновления базы в секундах. Рекомендуется оставить на 86400 (1 раз в день).
|
||||
# ***
|
||||
# Update interval for database updates in seconds. Recommended setting: 86400 (once per day).
|
||||
###
|
||||
interval = 86400
|
||||
|
||||
###
|
||||
# Язык на котором будут возвращаться данные об IP-адрессе.
|
||||
#
|
||||
# Допустимые значения:
|
||||
# Russian
|
||||
# English
|
||||
# Spanish
|
||||
# French
|
||||
# German
|
||||
# Japanese
|
||||
# Brazilian Portuguese
|
||||
# Simplified Chinese
|
||||
#
|
||||
# ***
|
||||
# The language in which IP address data will be returned.
|
||||
#
|
||||
# Acceptable values:
|
||||
# Russian
|
||||
# English
|
||||
# Spanish
|
||||
# French
|
||||
# German
|
||||
# Japanese
|
||||
# Brazilian Portuguese
|
||||
# Simplified Chinese
|
||||
#
|
||||
###
|
||||
language = "Russian"
|
||||
|
||||
###
|
||||
# Если по каким-то причинам захотите поменять адрес.
|
||||
# Например: у сервиса поменялся адрес или у Вас есть платная подписка.
|
||||
# Не забудьте поле измененяи параметра url убрать комментарий.
|
||||
# ***
|
||||
# If for some reason you want to change your address.
|
||||
# For example: the service's address has changed or you have a paid subscription.
|
||||
# Don't forget to remove the comment from the change field for the url parameter.
|
||||
###
|
||||
#url = ""
|
||||
@@ -22,13 +22,13 @@
|
||||
testing = true
|
||||
|
||||
###
|
||||
# Тестовый период, по истечении которого брандмауэр удалит правила и демон завершит работу.
|
||||
# Тестовый период, по истечении которого брандмауэр удалит правила, очистит другие данные и демон завершит работу.
|
||||
# Период указывается в минутах.
|
||||
# Мин: 1
|
||||
# Макс: 30000
|
||||
# По умолчанию: 5
|
||||
# ***
|
||||
# The test period after which the firewall will clear the rules and the daemon will shut down.
|
||||
# A test period after which the firewall will remove rules, clear other data, and the daemon will exit.
|
||||
# The period is specified in minutes.
|
||||
# Min: 1
|
||||
# Max: 30000
|
||||
@@ -76,6 +76,18 @@ pid_file = "/var/run/kor-elf-shield/kor-elf-shield.pid"
|
||||
###
|
||||
socket_file = "/var/run/kor-elf-shield/kor-elf-shield.sock"
|
||||
|
||||
###
|
||||
# Каталог для постоянных данных приложения (state): локальная база данных, кэш/индексы, файлы состояния
|
||||
# и другие служебные файлы. Должен быть доступен на запись пользователю, от имени которого запущен демон.
|
||||
# Если каталог не существует — будет создан.
|
||||
# По умолчанию: "/var/lib/kor-elf-shield/"
|
||||
# ***
|
||||
# Directory for persistent application data (state): local database, cache/indexes, state files, and other
|
||||
# internal data. Must be writable by the daemon user. If the directory does not exist, it will be created.
|
||||
# Default: "/var/lib/kor-elf-shield/"
|
||||
###
|
||||
data_dir = "/var/lib/kor-elf-shield/"
|
||||
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Log
|
||||
# ***
|
||||
@@ -153,12 +165,12 @@ encoding = "json"
|
||||
|
||||
###
|
||||
# Список мест для основных логов.
|
||||
# Можно указать несколько путей. Например output_paths = ["stdout", "/var/log/kor-elf-shield.log"].
|
||||
# Можно указать несколько путей. Например paths = ["stdout", "/var/log/kor-elf-shield.log"].
|
||||
# Где "stdout" - вывод данных в консоль.
|
||||
# По умолчанию: ["/var/log/kor-elf-shield.log"]
|
||||
# ***
|
||||
# List of locations for main logs.
|
||||
# Multiple paths can be specified. For example, output_paths = ["stdout", "/var/log/kor-elf-shield.log"].
|
||||
# Multiple paths can be specified. For example, paths = ["stdout", "/var/log/kor-elf-shield.log"].
|
||||
# Where "stdout" is the console output.
|
||||
# Default: ["/var/log/kor-elf-shield.log"]
|
||||
###
|
||||
@@ -166,12 +178,12 @@ paths = ["/var/log/kor-elf-shield.log"]
|
||||
|
||||
###
|
||||
# Список мест для ошибок логгирования. Например журнал не смог записать какие-то данные.
|
||||
# Можно указать несколько путей. Например output_paths = ["stderr", "/var/log/kor-elf-shield-log-error.log"].
|
||||
# Можно указать несколько путей. Например log_error_paths = ["stderr", "/var/log/kor-elf-shield-log-error.log"].
|
||||
# Где "stderr" - вывод данных в консоль.
|
||||
# По умолчанию: ["stderr"]
|
||||
# ***
|
||||
# List of locations for logging errors. For example, the log failed to write some data.
|
||||
# Multiple paths can be specified. For example, output_paths = ["stderr", "/var/log/kor-elf-shield-log-error.log"].
|
||||
# Multiple paths can be specified. For example, log_error_paths = ["stderr", "/var/log/kor-elf-shield-log-error.log"].
|
||||
# Where "stderr" is the data output to the console.
|
||||
# Default: ["stderr"]
|
||||
###
|
||||
@@ -261,3 +273,25 @@ analyzer = "/etc/kor-elf-shield/analyzer.toml"
|
||||
# Default: /etc/kor-elf-shield/docker.toml
|
||||
###
|
||||
docker = "/etc/kor-elf-shield/docker.toml"
|
||||
|
||||
###
|
||||
# Укажите путь к настройкам для получения списков блокировки IP-адресов.
|
||||
# Файл должен иметь расширение .toml.
|
||||
# По умолчанию: /etc/kor-elf-shield/blocklists.toml
|
||||
# ***
|
||||
# Specify the path to the settings for retrieving IP address block lists.
|
||||
# The file must have the .toml extension.
|
||||
# Default: /etc/kor-elf-shield/blocklists.toml
|
||||
###
|
||||
blocklists = "/etc/kor-elf-shield/blocklists.toml"
|
||||
|
||||
###
|
||||
# Укажите путь к настройкам для получения данных об IP-адресах.
|
||||
# Файл должен иметь расширение .toml.
|
||||
# По умолчанию: /etc/kor-elf-shield/geoip.toml
|
||||
# ***
|
||||
# Specify the path to the settings for obtaining data on IP addresses.
|
||||
# The file must have the .toml extension.
|
||||
# Default: /etc/kor-elf-shield/geoip.toml
|
||||
###
|
||||
geoip = "/etc/kor-elf-shield/geoip.toml"
|
||||
|
||||
@@ -21,6 +21,35 @@
|
||||
###
|
||||
enabled = false
|
||||
|
||||
###
|
||||
# Включает повторные попытки отправить уведомление, если сразу не получилось.
|
||||
# false = Выключает.
|
||||
# true = Включает.
|
||||
#
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables repeated attempts to send a notification if the first attempt fails.
|
||||
# false = Disables.
|
||||
# true = Enables.
|
||||
#
|
||||
# Default: true
|
||||
###
|
||||
enable_retries = true
|
||||
|
||||
###
|
||||
# Интервал времени в секундах между попытками.
|
||||
#
|
||||
# По умолчанию: 600
|
||||
# ***
|
||||
# The time interval in seconds between attempts.
|
||||
#
|
||||
# Default: 600
|
||||
###
|
||||
retry_interval = 600
|
||||
|
||||
|
||||
|
||||
|
||||
###
|
||||
# Название сервера в уведомлениях
|
||||
# По умолчанию: server
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
module git.kor-elf.net/kor-elf-shield/kor-elf-shield
|
||||
|
||||
go 1.25
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||
git.kor-elf.net/kor-elf-shield/blocklist v1.2.0
|
||||
git.kor-elf.net/kor-elf-shield/geoip2 v0.1.2
|
||||
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.2.1
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/nxadm/tail v1.4.11
|
||||
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
|
||||
github.com/urfave/cli/v3 v3.10.1
|
||||
github.com/wneessen/go-mail v0.8.1
|
||||
go.etcd.io/bbolt v1.5.0
|
||||
go.uber.org/zap v1.28.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/text v0.41.0
|
||||
)
|
||||
|
||||
require (
|
||||
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
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/oschwald/geoip2-golang/v2 v2.3.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
)
|
||||
|
||||
@@ -1,25 +1,46 @@
|
||||
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=
|
||||
git.kor-elf.net/kor-elf-shield/blocklist v1.1.0 h1:NS8be3TFBsUn+ft3oG5sAD56iJTGOkFH6GgjepEnS0s=
|
||||
git.kor-elf.net/kor-elf-shield/blocklist v1.1.0/go.mod h1:nNbQux5vbuoCa3wMiC2QsLb4tO1JLCssGzdljizcJUs=
|
||||
git.kor-elf.net/kor-elf-shield/blocklist v1.2.0 h1:oj4P4N5sa0kh2Zob0Mf/GrjqNAQrZjuX9/RoDx3UUGk=
|
||||
git.kor-elf.net/kor-elf-shield/blocklist v1.2.0/go.mod h1:nNbQux5vbuoCa3wMiC2QsLb4tO1JLCssGzdljizcJUs=
|
||||
git.kor-elf.net/kor-elf-shield/geoip2 v0.1.2 h1:/J9U+h9H92hW6TtwCznkRANqhX5kvBpN4uV7xDbwXpM=
|
||||
git.kor-elf.net/kor-elf-shield/geoip2 v0.1.2/go.mod h1:ULMUjpd2I9ikkDDE69IlpKT4vR2/nlYT0cqoR2T95sM=
|
||||
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.2.1 h1:B5u1uCYyrDlDlCSA03o/Djt/T0A3SgCeFsfZkq25Hwg=
|
||||
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.2.1/go.mod h1:a7F+XdL1pK5P3ucQRR2EK/fABAP37LLBENiA4hX7L6A=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||
github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo=
|
||||
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
|
||||
github.com/oschwald/geoip2-golang/v2 v2.3.0 h1:hT8/BT137lPJXq0DXwGQUS228k8pEhgBRJ1B70eqyAk=
|
||||
github.com/oschwald/geoip2-golang/v2 v2.3.0/go.mod h1:tHUYg65ssvQSSzSCkiFR6LWJPYOvSw/85JiBp8kXz0U=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.5.0 h1:WvEHCE8HwFS5pKWhW8nvvRxNzczuRUOGBLn2L03VlEQ=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.5.0/go.mod h1:EBnvLGgY+aSckqcgyfB5LPDviqaWdMZPBDwu8c2jJbs=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
@@ -38,24 +59,48 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
|
||||
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
|
||||
github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
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=
|
||||
github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
|
||||
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||
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=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CmdBlock() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "block",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.Usage"),
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.add.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.block.add.Description"),
|
||||
Action: cmdBlockAdd,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "port",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.add.FlagUsage.port"),
|
||||
},
|
||||
&cli.Uint32Flag{
|
||||
Name: "seconds",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.add.FlagUsage.seconds"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "reason",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.add.FlagUsage.reason"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.delete.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.block.delete.Description"),
|
||||
Action: cmdBlockDelete,
|
||||
},
|
||||
{
|
||||
Name: "clear",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.clear.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.block.clear.Description"),
|
||||
Action: cmdBlockClear,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func cmdBlockAdd(_ context.Context, cmd *cli.Command) error {
|
||||
ip := net.ParseIP(cmd.Args().Get(0))
|
||||
if ip == nil {
|
||||
return errors.New("invalid ip address")
|
||||
}
|
||||
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.SendCommand("block_add_ip", map[string]string{
|
||||
"ip": ip.String(),
|
||||
"port": cmd.String("port"),
|
||||
"seconds": strconv.Itoa(int(cmd.Uint32("seconds"))),
|
||||
"reason": cmd.String("reason"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result != "ok" {
|
||||
return errors.New(i18n.Lang.T("cmd.error", map[string]any{
|
||||
"Error": result,
|
||||
}))
|
||||
}
|
||||
|
||||
fmt.Println(i18n.Lang.T("block_add_ip_success"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdBlockDelete(_ context.Context, cmd *cli.Command) error {
|
||||
ip := net.ParseIP(cmd.Args().Get(0))
|
||||
if ip == nil {
|
||||
return errors.New("invalid ip address")
|
||||
}
|
||||
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.SendCommand("block_delete_ip", map[string]string{
|
||||
"ip": ip.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result != "ok" {
|
||||
return errors.New(i18n.Lang.T("cmd.error", map[string]any{
|
||||
"Error": result,
|
||||
}))
|
||||
}
|
||||
|
||||
fmt.Println(i18n.Lang.T("block_delete_ip_success"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdBlockClear(_ context.Context, _ *cli.Command) error {
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.Send("block_clear")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result != "ok" {
|
||||
return errors.New(i18n.Lang.T("block_clear_error"))
|
||||
}
|
||||
|
||||
fmt.Println(i18n.Lang.T("block_clear_success"))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"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"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CmdConfig() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "config",
|
||||
Usage: i18n.Lang.T("cmd.daemon.config.Usage"),
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "test",
|
||||
Usage: i18n.Lang.T("cmd.daemon.config.test.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.config.test.Description"),
|
||||
Action: CmdTestConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CmdTestConfig(_ context.Context, _ *cli.Command) error {
|
||||
falseLogger := log.NewFalseLogger()
|
||||
|
||||
testMain := testMainConfig()
|
||||
testDocker, dockerSupport := testDockerConfig()
|
||||
testFirewall := testFirewallConfig(dockerSupport)
|
||||
testAnalyzer := testAnalyzerConfig()
|
||||
testNotifications := testNotificationsConfig()
|
||||
testBlocklists := testBlocklistsConfig(falseLogger)
|
||||
testGeoip := testGeoipConfig(falseLogger)
|
||||
|
||||
checkNft := checkProgramNFT()
|
||||
checkJournalctl := checkProgramJournalctl()
|
||||
checkDocker := ""
|
||||
if dockerSupport {
|
||||
checkDocker = "\n " + checkProgramDocker()
|
||||
}
|
||||
|
||||
fmt.Println(
|
||||
"***\n"+i18n.Lang.T("cmd.daemon.config.test.settingTitle"),
|
||||
"\n", testMain,
|
||||
"\n", testFirewall,
|
||||
"\n", testAnalyzer,
|
||||
"\n", testNotifications,
|
||||
"\n", testDocker,
|
||||
"\n", testBlocklists,
|
||||
"\n", testGeoip,
|
||||
"\n"+i18n.Lang.T("cmd.daemon.config.test.checkingPrograms"),
|
||||
"\n", checkNft,
|
||||
"\n", checkJournalctl,
|
||||
checkDocker,
|
||||
"\n***",
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func testMainConfig() string {
|
||||
configTitle := i18n.Lang.T("cmd.daemon.config.test.main")
|
||||
if err := setting.Config.Validate(); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
if err := setting.Config.ValidateBeforeStart(); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func testDockerConfig() (message string, dockerSupport bool) {
|
||||
configTitle := "docker"
|
||||
_, dockerSupport, err := setting.Config.OtherSettingsPath.ToDockerConfig(setting.Config.BinaryLocations)
|
||||
if err != nil {
|
||||
return resultError(configTitle, err), false
|
||||
}
|
||||
|
||||
return resultOk(configTitle), dockerSupport
|
||||
}
|
||||
|
||||
func testFirewallConfig(dockerSupport bool) string {
|
||||
configTitle := "firewall"
|
||||
if _, _, err := setting.Config.OtherSettingsPath.ToFirewallConfig(dockerSupport); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func testAnalyzerConfig() string {
|
||||
configTitle := "analyzer"
|
||||
if _, err := setting.Config.OtherSettingsPath.ToAnalyzerConfig(setting.Config.BinaryLocations); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func testNotificationsConfig() string {
|
||||
configTitle := "notifications"
|
||||
if _, err := setting.Config.OtherSettingsPath.ToNotificationsConfig(); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func testBlocklistsConfig(logger log.Logger) string {
|
||||
configTitle := "blocklists"
|
||||
if _, _, err := setting.Config.OtherSettingsPath.ToBlocklistConfig(logger); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func testGeoipConfig(logger log.Logger) string {
|
||||
configTitle := "geoip"
|
||||
if _, _, err := setting.Config.OtherSettingsPath.ToConfig(setting.Config.DataDir, logger); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func checkProgramNFT() string {
|
||||
programTitle := "nftables"
|
||||
path := setting.Config.BinaryLocations.Nftables
|
||||
if path == "" {
|
||||
return resultError(programTitle, errors.New(i18n.Lang.T("cmd.daemon.config.test.pathEmpty", map[string]interface{}{"Program": programTitle})))
|
||||
}
|
||||
|
||||
cmd := exec.Command(path, "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return resultError(programTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(programTitle)
|
||||
}
|
||||
|
||||
func checkProgramJournalctl() string {
|
||||
programTitle := "journalctl"
|
||||
path := setting.Config.BinaryLocations.Journalctl
|
||||
if path == "" {
|
||||
return resultError(programTitle, errors.New(i18n.Lang.T("cmd.daemon.config.test.pathEmpty", map[string]interface{}{"Program": programTitle})))
|
||||
}
|
||||
|
||||
cmd := exec.Command(path, "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return resultError(programTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(programTitle)
|
||||
}
|
||||
|
||||
func checkProgramDocker() string {
|
||||
programTitle := "docker"
|
||||
path := setting.Config.BinaryLocations.Docker
|
||||
if path == "" {
|
||||
return resultError(programTitle, errors.New(i18n.Lang.T("cmd.daemon.config.test.pathEmpty", map[string]interface{}{"Program": programTitle})))
|
||||
}
|
||||
|
||||
cmd := exec.Command(path, "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return resultError(programTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(programTitle)
|
||||
}
|
||||
|
||||
func resultOk(title string) string {
|
||||
return fmt.Sprintf("%s: \033[32mOk\033[0m", title)
|
||||
}
|
||||
|
||||
func resultError(title string, err error) string {
|
||||
errText := i18n.Lang.T("cmd.daemon.config.test.error", map[string]interface{}{"Error": err})
|
||||
return fmt.Sprintf("%s: \033[31mError\n %s\u001B[0m", title, errText)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CmdGeoIP() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "geoip",
|
||||
Usage: i18n.Lang.T("cmd.daemon.geoip.Usage"),
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "info",
|
||||
Usage: i18n.Lang.T("cmd.daemon.geoip.info.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.geoip.info.Description"),
|
||||
Action: CmdGeoIPInfo,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "ip",
|
||||
Usage: i18n.Lang.T("cmd.daemon.geoip.info.FlagUsage.ip"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "refresh",
|
||||
Usage: i18n.Lang.T("cmd.daemon.geoip.refresh.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.geoip.refresh.Description"),
|
||||
Action: CmdGeoIPRefresh,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CmdGeoIPInfo(_ context.Context, cmd *cli.Command) error {
|
||||
ip := net.ParseIP(cmd.Args().Get(0))
|
||||
if ip == nil {
|
||||
return errors.New("invalid ip address")
|
||||
}
|
||||
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.SendCommand("geoip_info", map[string]string{
|
||||
"ip": ip.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(result)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CmdGeoIPRefresh(_ context.Context, _ *cli.Command) error {
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.Send("geoip_refresh")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result != "ok" {
|
||||
return errors.New(i18n.Lang.T("cmd.error", map[string]any{
|
||||
"Error": result,
|
||||
}))
|
||||
}
|
||||
|
||||
fmt.Println(i18n.Lang.T("geoip_refresh_success"))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/socket"
|
||||
)
|
||||
|
||||
func newSocket() (socket.Client, error) {
|
||||
if setting.Config.SocketFile == "" {
|
||||
return nil, errors.New(i18n.Lang.T("socket file is not specified"))
|
||||
}
|
||||
return socket.NewSocketClient(setting.Config.SocketFile)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CmdNotifications() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "notifications",
|
||||
Usage: i18n.Lang.T("cmd.daemon.notifications.Usage"),
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "queue",
|
||||
Usage: i18n.Lang.T("cmd.daemon.notifications.queue.Usage"),
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "count",
|
||||
Usage: i18n.Lang.T("cmd.daemon.notifications.queue.count.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.notifications.queue.count.Description"),
|
||||
Action: cmdNotificationsQueueCount,
|
||||
},
|
||||
{
|
||||
Name: "clear",
|
||||
Usage: i18n.Lang.T("cmd.daemon.notifications.queue.clear.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.notifications.queue.clear.Description"),
|
||||
Action: cmdNotificationsQueueClear,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func cmdNotificationsQueueCount(_ context.Context, _ *cli.Command) error {
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.Send("notifications_queue_count")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(i18n.Lang.T("cmd.daemon.notifications.queue.count.result", map[string]interface{}{
|
||||
"Count": result,
|
||||
}))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdNotificationsQueueClear(_ context.Context, _ *cli.Command) error {
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.Send("notifications_queue_clear")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result != "ok" {
|
||||
return errors.New(i18n.Lang.T("notifications_queue_clear_error"))
|
||||
}
|
||||
|
||||
fmt.Println(i18n.Lang.T("notifications_queue_clear_success"))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/socket"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -21,11 +19,7 @@ func CmdReopenLogger() *cli.Command {
|
||||
}
|
||||
|
||||
func cmdReopenLogger(_ context.Context, _ *cli.Command) error {
|
||||
if setting.Config.SocketFile == "" {
|
||||
return errors.New(i18n.Lang.T("socket file is not specified"))
|
||||
}
|
||||
|
||||
sock, err := socket.NewSocketClient(setting.Config.SocketFile)
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
|
||||
@@ -3,14 +3,19 @@ package daemon
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/blocklist"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/repository"
|
||||
"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/geoip"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/info"
|
||||
"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"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -51,7 +56,20 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
notificationsService, err := newNotificationsService(logger)
|
||||
repositories, err := db.New(config.DataDir)
|
||||
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
|
||||
}
|
||||
defer func() {
|
||||
_ = repositories.Close()
|
||||
}()
|
||||
config.Repositories = repositories
|
||||
|
||||
notificationsService, err := newNotificationsService(repositories.NotificationsQueue(), logger)
|
||||
if err != nil {
|
||||
logger.Fatal(err.Error())
|
||||
|
||||
@@ -60,7 +78,16 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
d, err := daemon.NewDaemon(config, logger, notificationsService, dockerService)
|
||||
blocklistService := newBlocklistService(ctx, repositories.Blocklist(), logger)
|
||||
|
||||
geoIPService := newGeoIPService(config.DataDir, logger)
|
||||
defer func() {
|
||||
_ = geoIPService.Close()
|
||||
}()
|
||||
|
||||
daemonInfo := newDaemonInfo(repositories.Metadata(), setting.Config.ListPathConfigFiles(), logger)
|
||||
|
||||
d, err := daemon.NewDaemon(daemonInfo, config, logger, notificationsService, dockerService, blocklistService, geoIPService)
|
||||
if err != nil {
|
||||
logger.Fatal(err.Error())
|
||||
|
||||
@@ -81,13 +108,30 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newNotificationsService(logger log.Logger) (notifications.Notifications, error) {
|
||||
func newDaemonInfo(repo repository.MetadataRepository, listPathFiles map[string]string, logger log.Logger) info.Info {
|
||||
metaFirewallFileNft := info.NewMetadataFirewallFileNft(repo)
|
||||
metadataContainer := info.NewMetadataContainer(metaFirewallFileNft)
|
||||
|
||||
return info.New(
|
||||
setting.AppVer,
|
||||
info.IsVersionChanged(repo, setting.AppVer, logger),
|
||||
|
||||
setting.AppBuiltWith,
|
||||
setting.AppStartTime,
|
||||
|
||||
info.IsSettingsChanged(repo, listPathFiles, logger),
|
||||
|
||||
metadataContainer,
|
||||
)
|
||||
}
|
||||
|
||||
func newNotificationsService(queueRepository repository.NotificationsQueueRepository, logger log.Logger) (notifications.Notifications, error) {
|
||||
config, err := setting.Config.OtherSettingsPath.ToNotificationsConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return notifications.New(config, logger), nil
|
||||
return notifications.New(config, queueRepository, logger), nil
|
||||
}
|
||||
|
||||
func newDockerService(ctx context.Context, logger log.Logger) (dockerService docker_monitor.Docker, dockerSupport bool, err error) {
|
||||
@@ -108,3 +152,41 @@ func newDockerService(ctx context.Context, logger log.Logger) (dockerService doc
|
||||
|
||||
return dockerService, dockerSupport, nil
|
||||
}
|
||||
|
||||
func newBlocklistService(ctx context.Context, blocklistRepository repository.BlocklistRepository, logger log.Logger) blocklist.Blocklist {
|
||||
config, isEnabled, err := setting.Config.OtherSettingsPath.ToBlocklistConfig(logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create blocklist service: %s", err))
|
||||
return blocklist.NewFalseBlocklist()
|
||||
}
|
||||
if !isEnabled {
|
||||
return blocklist.NewFalseBlocklist()
|
||||
}
|
||||
|
||||
blocklistConfig := blocklist.Config{
|
||||
BlocklistRepository: blocklistRepository,
|
||||
Sources: config,
|
||||
PathDir: strings.TrimRight(setting.Config.DataDir, "/") + "/blocklists",
|
||||
}
|
||||
|
||||
blocklistService, err := blocklist.New(blocklistConfig, ctx, logger)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
return blocklist.NewFalseBlocklist()
|
||||
}
|
||||
|
||||
return blocklistService
|
||||
}
|
||||
|
||||
func newGeoIPService(dataDir string, logger log.Logger) geoip.GeoIP {
|
||||
config, geoIPSupport, err := setting.Config.OtherSettingsPath.ToConfig(dataDir, logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create geoIP service: %s", err))
|
||||
return geoip.NewFalseGeoIP()
|
||||
}
|
||||
if !geoIPSupport || config.GeoIP == nil {
|
||||
return geoip.NewFalseGeoIP()
|
||||
}
|
||||
|
||||
return geoip.New(config, logger)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/socket"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -21,11 +19,7 @@ func CmdStatus() *cli.Command {
|
||||
}
|
||||
|
||||
func cmdStatus(_ context.Context, _ *cli.Command) error {
|
||||
if setting.Config.SocketFile == "" {
|
||||
return errors.New(i18n.Lang.T("socket file is not specified"))
|
||||
}
|
||||
|
||||
sock, err := socket.NewSocketClient(setting.Config.SocketFile)
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
@@ -38,11 +32,7 @@ func cmdStatus(_ context.Context, _ *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if result != "ok" {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
|
||||
fmt.Println("ok")
|
||||
fmt.Println(result)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/socket"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -21,11 +19,7 @@ func CmdStop() *cli.Command {
|
||||
}
|
||||
|
||||
func stopDaemon(_ context.Context, _ *cli.Command) error {
|
||||
if setting.Config.SocketFile == "" {
|
||||
return errors.New(i18n.Lang.T("socket file is not specified"))
|
||||
}
|
||||
|
||||
sock, err := socket.NewSocketClient(setting.Config.SocketFile)
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ func NewMainApp(appVer AppVersion, defaultConfigPath string) *cli.Command {
|
||||
daemon.CmdStart(),
|
||||
daemon.CmdStop(),
|
||||
daemon.CmdStatus(),
|
||||
daemon.CmdConfig(),
|
||||
daemon.CmdReopenLogger(),
|
||||
daemon.CmdNotifications(),
|
||||
daemon.CmdBlock(),
|
||||
daemon.CmdGeoIP(),
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
@@ -7,12 +7,16 @@ import (
|
||||
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/analyzer/log/analysis/brute_force_protection_group"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/geoip"
|
||||
"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)
|
||||
ClearDBData() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -21,40 +25,63 @@ type analyzer struct {
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
systemd analyzerLog.Systemd
|
||||
files analyzerLog.FileMonitoring
|
||||
analysis analyzerLog.Analysis
|
||||
|
||||
logChan chan analysisServices.Entry
|
||||
}
|
||||
|
||||
func New(config config2.Config, logger log.Logger, notify notifications.Notifications) Analyzer {
|
||||
var units []string
|
||||
func New(
|
||||
config config2.Config,
|
||||
blockService brute_force_protection_group.BlockService,
|
||||
repositories db.Repositories,
|
||||
logger log.Logger,
|
||||
notify notifications.Notifications,
|
||||
ipInfo geoip.Info,
|
||||
) Analyzer {
|
||||
var journalMatches []string
|
||||
journalMatchesUniq := map[string]struct{}{}
|
||||
|
||||
if config.Login.Enabled {
|
||||
if config.Login.SSH.Enabled {
|
||||
units = append(units, "_SYSTEMD_UNIT=ssh.service")
|
||||
var files []string
|
||||
filesUniq := map[string]struct{}{}
|
||||
|
||||
rulesIndex := analysisServices.NewRulesIndex()
|
||||
|
||||
for _, source := range config.Sources {
|
||||
switch source.Type {
|
||||
case config2.SourceTypeJournal:
|
||||
match := source.Journal.JournalctlMatch()
|
||||
if _, ok := journalMatchesUniq[match]; !ok {
|
||||
journalMatchesUniq[match] = struct{}{}
|
||||
journalMatches = append(journalMatches, match)
|
||||
}
|
||||
case config2.SourceTypeFile:
|
||||
file := source.File.Path
|
||||
if _, ok := filesUniq[file]; !ok {
|
||||
filesUniq[file] = struct{}{}
|
||||
files = append(files, file)
|
||||
}
|
||||
default:
|
||||
logger.Error(fmt.Sprintf("Unknown source type: %s", source.Type))
|
||||
continue
|
||||
}
|
||||
|
||||
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")
|
||||
err := rulesIndex.Add(source)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to add rule: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, units, logger)
|
||||
analysisService := analyzerLog.NewAnalysis(&config, logger, notify)
|
||||
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, journalMatches, logger)
|
||||
filesService := analyzerLog.NewFileMonitoring(files, logger)
|
||||
analysisService := analyzerLog.NewAnalysis(rulesIndex, blockService, repositories, logger, notify, ipInfo)
|
||||
|
||||
return &analyzer{
|
||||
config: config,
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
systemd: systemdService,
|
||||
files: filesService,
|
||||
analysis: analysisService,
|
||||
|
||||
logChan: make(chan analysisServices.Entry, 1000),
|
||||
@@ -62,11 +89,28 @@ func New(config config2.Config, logger log.Logger, notify notifications.Notifica
|
||||
}
|
||||
|
||||
func (a *analyzer) Run(ctx context.Context) {
|
||||
go a.systemd.Run(ctx, a.logChan)
|
||||
go a.processLogs(ctx)
|
||||
go a.systemd.Run(ctx, a.logChan)
|
||||
go a.files.Run(ctx, a.logChan)
|
||||
|
||||
a.logger.Debug("Analyzer is start")
|
||||
}
|
||||
|
||||
func (a *analyzer) ClearDBData() error {
|
||||
a.logger.Debug("Clear data")
|
||||
|
||||
clearDBErrors, err := a.analysis.ClearDBData()
|
||||
if err != nil {
|
||||
for _, err := range clearDBErrors {
|
||||
a.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *analyzer) processLogs(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
@@ -77,35 +121,20 @@ func (a *analyzer) processLogs(ctx context.Context) {
|
||||
// Channel closed
|
||||
return
|
||||
}
|
||||
a.logger.Debug(fmt.Sprintf("Received log entry: %s", entry))
|
||||
a.logger.Debug(fmt.Sprintf("Received log entry: %v", 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))
|
||||
}
|
||||
a.analysis.BruteForceProtection(&entry)
|
||||
a.analysis.Alert(&entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analyzer) Close() error {
|
||||
if err := a.systemd.Close(); err != nil {
|
||||
return err
|
||||
a.logger.Error(err.Error())
|
||||
}
|
||||
if err := a.files.Close(); err != nil {
|
||||
a.logger.Error(err.Error())
|
||||
}
|
||||
close(a.logChan)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type RateLimit struct {
|
||||
Count uint32
|
||||
Period uint32
|
||||
}
|
||||
|
||||
type AlertGroup struct {
|
||||
Name string
|
||||
Message string
|
||||
RateLimits []RateLimit
|
||||
RateLimitResetPeriod uint32
|
||||
}
|
||||
|
||||
func (g *AlertGroup) RateLimit(level uint64) (rateLimit RateLimit, err error) {
|
||||
lenRateLimits := len(g.RateLimits) - 1
|
||||
|
||||
if lenRateLimits < 0 {
|
||||
return RateLimit{}, fmt.Errorf("rate limits is empty")
|
||||
}
|
||||
|
||||
if level <= uint64(lenRateLimits) {
|
||||
rateLimit = g.RateLimits[level]
|
||||
} else {
|
||||
rateLimit = g.RateLimits[lenRateLimits]
|
||||
}
|
||||
|
||||
return rateLimit, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
|
||||
)
|
||||
|
||||
func NewBruteForceProtectionSSH(isNotify bool, notifyCooldown int, notifyEvery int, group *brute_force_protection.Group) ([]*Source, error) {
|
||||
var sources []*Source
|
||||
|
||||
journal, err := NewSourceJournal(JournalFieldSystemdUnit, "ssh.service")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create journal source for SSH: %w", err)
|
||||
}
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
BruteForceProtectionRule: &brute_force_protection.Rule{
|
||||
Name: "_ssh",
|
||||
Message: i18n.Lang.T("alert.bruteForceProtection.ssh.message"),
|
||||
|
||||
IsNotification: isNotify,
|
||||
NotificationCooldown: uint32(notifyCooldown),
|
||||
NotificationEvery: uint32(notifyEvery),
|
||||
|
||||
Patterns: []brute_force_protection.RegexPattern{
|
||||
{
|
||||
Regexp: regular_expression.NewLazyRegexp(`^Failed (password|publickey) for (invalid user |illegal user )?(\S*) from (\S+)( port \S+ \S+\s*)`),
|
||||
Values: []brute_force_protection.PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
Value: 3,
|
||||
},
|
||||
},
|
||||
IP: 4,
|
||||
},
|
||||
{
|
||||
Regexp: regular_expression.NewLazyRegexp(`^Invalid user (\S*) from (\S+) port (\S+)`),
|
||||
Values: []brute_force_protection.PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
IP: 2,
|
||||
},
|
||||
},
|
||||
Group: group,
|
||||
},
|
||||
}
|
||||
|
||||
sources = append(sources, source)
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package brute_force_protection
|
||||
|
||||
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
|
||||
type Block interface {
|
||||
PortsBlocked() (bool, []types.L4Port)
|
||||
}
|
||||
|
||||
type block struct {
|
||||
shouldPortsBlocked bool
|
||||
ports []types.L4Port
|
||||
}
|
||||
|
||||
func NewBlockOnceIPConfig() Block {
|
||||
return &block{
|
||||
shouldPortsBlocked: false,
|
||||
ports: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func NewBlockIPAndPortsConfig(ports []types.L4Port) Block {
|
||||
return &block{
|
||||
shouldPortsBlocked: true,
|
||||
ports: ports,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *block) PortsBlocked() (bool, []types.L4Port) {
|
||||
if !b.shouldPortsBlocked {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, b.ports
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package brute_force_protection
|
||||
|
||||
import (
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/partition"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
Name string
|
||||
Message string
|
||||
|
||||
IsNotification bool
|
||||
NotificationCooldown uint32
|
||||
NotificationEvery uint32
|
||||
|
||||
Patterns []RegexPattern
|
||||
Group *Group
|
||||
}
|
||||
|
||||
type RegexPattern struct {
|
||||
Regexp *regular_expression.LazyRegexp
|
||||
Values []PatternValue
|
||||
IP uint8
|
||||
Partition *partition.PatternPartition
|
||||
}
|
||||
|
||||
type RateLimit struct {
|
||||
Count uint32
|
||||
Period uint32
|
||||
BlockingTimeSeconds uint32
|
||||
BlockConfig Block
|
||||
}
|
||||
|
||||
type PatternValue struct {
|
||||
Name string
|
||||
Value uint8
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package brute_force_protection
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Group struct {
|
||||
Name string
|
||||
Message string
|
||||
RateLimits []RateLimit
|
||||
RateLimitResetPeriod uint32
|
||||
}
|
||||
|
||||
func (g *Group) RateLimit(level uint64) (rateLimit RateLimit, err error) {
|
||||
lenRateLimits := len(g.RateLimits) - 1
|
||||
|
||||
if lenRateLimits < 0 {
|
||||
return RateLimit{}, fmt.Errorf("rate limits is empty")
|
||||
}
|
||||
|
||||
if level <= uint64(lenRateLimits) {
|
||||
rateLimit = g.RateLimits[level]
|
||||
} else {
|
||||
rateLimit = g.RateLimits[lenRateLimits]
|
||||
}
|
||||
|
||||
return rateLimit, nil
|
||||
}
|
||||
@@ -1,6 +1,131 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/partition"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
)
|
||||
|
||||
var (
|
||||
reSystemdUnitValue = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._@-]{0,255}\.(service|socket|target|mount|timer|path|scope|slice|device)$`)
|
||||
reSyslogIDValue = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._@-]{0,127}$`)
|
||||
)
|
||||
|
||||
type SourceType string
|
||||
|
||||
const (
|
||||
SourceTypeJournal SourceType = "journalctl"
|
||||
SourceTypeFile SourceType = "file"
|
||||
)
|
||||
|
||||
type JournalField string
|
||||
|
||||
const (
|
||||
JournalFieldSystemdUnit JournalField = "_SYSTEMD_UNIT"
|
||||
JournalFieldSyslogIdentifier JournalField = "SYSLOG_IDENTIFIER"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
BinPath BinPath
|
||||
Login Login
|
||||
Sources []*Source
|
||||
}
|
||||
|
||||
type SourceJournal struct {
|
||||
Field JournalField
|
||||
Match string
|
||||
}
|
||||
|
||||
func NewSourceJournal(field JournalField, match string) (*SourceJournal, error) {
|
||||
v := strings.TrimSpace(match)
|
||||
if v == "" {
|
||||
return nil, fmt.Errorf("journal match must not be empty")
|
||||
}
|
||||
if len(v) > 512 {
|
||||
return nil, fmt.Errorf("journal match is too long: %d", len(v))
|
||||
}
|
||||
for _, r := range v {
|
||||
if r == 0 || r == '\n' || r == '\r' || unicode.IsControl(r) {
|
||||
return nil, fmt.Errorf("journal match contains control characters")
|
||||
}
|
||||
}
|
||||
// to avoid breaking the FIELD=VALUE format and concatenation with '+'
|
||||
if strings.ContainsAny(v, "=+") {
|
||||
return nil, fmt.Errorf("journal match must not contain '=' or '+'")
|
||||
}
|
||||
|
||||
if strings.ContainsAny(v, " \t") {
|
||||
return nil, fmt.Errorf("journal match must not contain spaces or tabs")
|
||||
}
|
||||
|
||||
switch field {
|
||||
case JournalFieldSystemdUnit:
|
||||
if !reSystemdUnitValue.MatchString(v) {
|
||||
return nil, fmt.Errorf("invalid _SYSTEMD_UNIT value: %q", v)
|
||||
}
|
||||
case JournalFieldSyslogIdentifier:
|
||||
if !reSyslogIDValue.MatchString(v) {
|
||||
return nil, fmt.Errorf("invalid SYSLOG_IDENTIFIER value: %q", v)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid journal field: %q", field)
|
||||
}
|
||||
|
||||
return &SourceJournal{Field: field, Match: v}, nil
|
||||
}
|
||||
|
||||
type SourceFile struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func NewSourceFile(path string) (*SourceFile, error) {
|
||||
if err := validate.PathFile(path, "logAlert.rules.source.path"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SourceFile{Path: path}, nil
|
||||
}
|
||||
|
||||
func (s *SourceJournal) JournalctlMatch() string {
|
||||
return string(s.Field) + "=" + s.Match
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Type SourceType
|
||||
Journal *SourceJournal
|
||||
File *SourceFile
|
||||
|
||||
AlertRule *AlertRule
|
||||
BruteForceProtectionRule *brute_force_protection.Rule
|
||||
}
|
||||
|
||||
type AlertRule struct {
|
||||
Name string
|
||||
Message string
|
||||
IsNotification bool
|
||||
Patterns []AlertRegexPattern
|
||||
Group *AlertGroup
|
||||
}
|
||||
|
||||
type AlertRegexPattern struct {
|
||||
Regexp *regular_expression.LazyRegexp
|
||||
Values []PatternValue
|
||||
Partition *partition.PatternPartition
|
||||
}
|
||||
|
||||
type PatternValue struct {
|
||||
Name string
|
||||
Value uint8
|
||||
Type PatternTypeValue
|
||||
}
|
||||
|
||||
type PatternTypeValue string
|
||||
|
||||
const (
|
||||
PatternValueIP PatternTypeValue = "ip"
|
||||
)
|
||||
|
||||
@@ -1,30 +1,161 @@
|
||||
package config
|
||||
|
||||
type Login struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
SSH LoginSSH
|
||||
Local LoginLocal
|
||||
Su LoginSu
|
||||
Sudo LoginSudo
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
|
||||
)
|
||||
|
||||
func NewLoginSSH(isNotify bool) ([]*Source, error) {
|
||||
var sources []*Source
|
||||
|
||||
journal, err := NewSourceJournal(JournalFieldSystemdUnit, "ssh.service")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create journal source for SSH login: %w", err)
|
||||
}
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
AlertRule: &AlertRule{
|
||||
Name: "_login-ssh",
|
||||
Message: i18n.Lang.T("alert.login.ssh.message"),
|
||||
IsNotification: isNotify,
|
||||
Patterns: []AlertRegexPattern{
|
||||
{
|
||||
Regexp: regular_expression.NewLazyRegexp(`^Accepted (\S+) for (\S+) from (\S+) port \S+`),
|
||||
Values: []PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
Value: 2,
|
||||
},
|
||||
{
|
||||
Name: "IP",
|
||||
Value: 3,
|
||||
Type: PatternValueIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Group: nil,
|
||||
},
|
||||
}
|
||||
|
||||
sources = append(sources, source)
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
type LoginSSH struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
func NewLoginLocal(isNotify bool) ([]*Source, error) {
|
||||
var sources []*Source
|
||||
|
||||
journal, err := NewSourceJournal(JournalFieldSyslogIdentifier, "login")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create journal source for local login: %w", err)
|
||||
}
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
AlertRule: &AlertRule{
|
||||
Name: "_login-local",
|
||||
Message: i18n.Lang.T("alert.login.local.message"),
|
||||
IsNotification: isNotify,
|
||||
Patterns: []AlertRegexPattern{
|
||||
{
|
||||
Regexp: regular_expression.NewLazyRegexp(`^pam_unix\(login:session\): session opened for user (\S+)\(\S+\) by \S+`),
|
||||
Values: []PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Group: nil,
|
||||
},
|
||||
}
|
||||
|
||||
sources = append(sources, source)
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
type LoginLocal struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
func NewLoginSu(isNotify bool) ([]*Source, error) {
|
||||
var sources []*Source
|
||||
|
||||
journal, err := NewSourceJournal(JournalFieldSyslogIdentifier, "su")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create journal source for su login: %w", err)
|
||||
}
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
AlertRule: &AlertRule{
|
||||
Name: "_login-su",
|
||||
Message: i18n.Lang.T("alert.login.su.message"),
|
||||
IsNotification: isNotify,
|
||||
Patterns: []AlertRegexPattern{
|
||||
{
|
||||
Regexp: regular_expression.NewLazyRegexp(`^pam_unix\(su:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`),
|
||||
Values: []PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
Value: 2,
|
||||
},
|
||||
{
|
||||
Name: i18n.Lang.T("access to user has been gained"),
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Group: nil,
|
||||
},
|
||||
}
|
||||
|
||||
sources = append(sources, source)
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
type LoginSu struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
}
|
||||
func NewLoginSudo(isNotify bool) ([]*Source, error) {
|
||||
var sources []*Source
|
||||
|
||||
type LoginSudo struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
journal, err := NewSourceJournal(JournalFieldSyslogIdentifier, "sudo")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create journal source for sudo login: %w", err)
|
||||
}
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
AlertRule: &AlertRule{
|
||||
Name: "_login-sudo",
|
||||
Message: i18n.Lang.T("alert.login.sudo.message"),
|
||||
IsNotification: isNotify,
|
||||
Patterns: []AlertRegexPattern{
|
||||
{
|
||||
Regexp: regular_expression.NewLazyRegexp(`^pam_unix\(sudo:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`),
|
||||
Values: []PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
Value: 2,
|
||||
},
|
||||
{
|
||||
Name: i18n.Lang.T("access to user has been gained"),
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Group: nil,
|
||||
},
|
||||
}
|
||||
|
||||
sources = append(sources, source)
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package partition
|
||||
|
||||
type exceptType struct {
|
||||
keywords []string
|
||||
partitioned bool
|
||||
}
|
||||
|
||||
func NewExceptType(
|
||||
keywords []string,
|
||||
partitioned bool,
|
||||
) PatternPartitionType {
|
||||
return &exceptType{
|
||||
keywords: keywords,
|
||||
partitioned: partitioned,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *exceptType) IsPartitioned() bool {
|
||||
return o.partitioned
|
||||
}
|
||||
|
||||
func (o *exceptType) Accepts(text string) (bool, string) {
|
||||
for _, keyword := range o.keywords {
|
||||
if keyword == text {
|
||||
return false, text
|
||||
}
|
||||
}
|
||||
return true, text
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package partition
|
||||
|
||||
type onlyType struct {
|
||||
keywords []string
|
||||
partitioned bool
|
||||
}
|
||||
|
||||
func NewOnlyType(
|
||||
keywords []string,
|
||||
partitioned bool,
|
||||
) PatternPartitionType {
|
||||
return &onlyType{
|
||||
keywords: keywords,
|
||||
partitioned: partitioned,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *onlyType) IsPartitioned() bool {
|
||||
return o.partitioned
|
||||
}
|
||||
|
||||
func (o *onlyType) Accepts(text string) (bool, string) {
|
||||
for _, keyword := range o.keywords {
|
||||
if keyword == text {
|
||||
return true, text
|
||||
}
|
||||
}
|
||||
return false, text
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package partition
|
||||
|
||||
import "strings"
|
||||
|
||||
type PatternPartitionType interface {
|
||||
Accepts(text string) (bool, string)
|
||||
IsPartitioned() bool
|
||||
}
|
||||
|
||||
type PatternPartition struct {
|
||||
Value uint8
|
||||
Type PatternPartitionType
|
||||
Normalize func(keyword string) string
|
||||
}
|
||||
|
||||
type Normalize interface {
|
||||
Normalize(text string) string
|
||||
}
|
||||
|
||||
type normalize struct {
|
||||
Trim bool
|
||||
LowerCase bool
|
||||
}
|
||||
|
||||
func NewNormalize(trim, lowerCase bool) Normalize {
|
||||
return &normalize{
|
||||
Trim: trim,
|
||||
LowerCase: lowerCase,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *normalize) Normalize(text string) string {
|
||||
if p.LowerCase {
|
||||
text = strings.ToLower(text)
|
||||
}
|
||||
if p.Trim {
|
||||
text = strings.TrimSpace(text)
|
||||
}
|
||||
return text
|
||||
}
|
||||
@@ -1,52 +1,75 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
"fmt"
|
||||
|
||||
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/analyzer/log/analysis/alert_group"
|
||||
analysisBruteForceProtection "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection_group"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/geoip"
|
||||
"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
|
||||
Alert(entry *analysisServices.Entry)
|
||||
BruteForceProtection(entry *analysisServices.Entry)
|
||||
ClearDBData() ([]error, error)
|
||||
}
|
||||
|
||||
type analysis struct {
|
||||
sshService analysisServices.Analysis
|
||||
localeService analysisServices.Analysis
|
||||
suService analysisServices.Analysis
|
||||
sudoService analysisServices.Analysis
|
||||
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
alertService analysisServices.Alert
|
||||
bruteForceProtectionService analysisServices.BruteForceProtection
|
||||
}
|
||||
|
||||
func NewAnalysis(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
||||
func NewAnalysis(
|
||||
rulesIndex *analysisServices.RulesIndex,
|
||||
blockService brute_force_protection_group.BlockService,
|
||||
repositories db.Repositories,
|
||||
logger log.Logger,
|
||||
notify notifications.Notifications,
|
||||
ipInfo geoip.Info,
|
||||
) Analysis {
|
||||
alertGroupService := alert_group.NewGroup(repositories.AlertGroup(), logger)
|
||||
bruteForceProtectionGroupService := brute_force_protection_group.NewGroup(repositories.BruteForceProtectionGroup(), logger)
|
||||
notificationPolicy := analysisBruteForceProtection.NewNotificationPolicy(repositories.BruteForceProtectionNotifyPolicy(), logger)
|
||||
|
||||
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,
|
||||
alertService: analysisServices.NewAlert(rulesIndex, alertGroupService, logger, notify, ipInfo),
|
||||
bruteForceProtectionService: analysisServices.NewBruteForceProtection(
|
||||
rulesIndex,
|
||||
bruteForceProtectionGroupService,
|
||||
blockService,
|
||||
notificationPolicy,
|
||||
logger,
|
||||
notify,
|
||||
ipInfo,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analysis) SSH(entry *analysisServices.Entry) error {
|
||||
return a.sshService.Process(entry)
|
||||
func (a *analysis) Alert(entry *analysisServices.Entry) {
|
||||
a.alertService.Analyze(entry)
|
||||
}
|
||||
|
||||
func (a *analysis) Locale(entry *analysisServices.Entry) error {
|
||||
return a.localeService.Process(entry)
|
||||
func (a *analysis) BruteForceProtection(entry *analysisServices.Entry) {
|
||||
a.bruteForceProtectionService.Analyze(entry)
|
||||
}
|
||||
|
||||
func (a *analysis) Su(entry *analysisServices.Entry) error {
|
||||
return a.suService.Process(entry)
|
||||
}
|
||||
func (a *analysis) ClearDBData() ([]error, error) {
|
||||
var errClearDB []error
|
||||
if err := a.alertService.ClearDBData(); err != nil {
|
||||
errClearDB = append(errClearDB, err)
|
||||
}
|
||||
if err := a.bruteForceProtectionService.ClearDBData(); err != nil {
|
||||
errClearDB = append(errClearDB, err)
|
||||
}
|
||||
|
||||
func (a *analysis) Sudo(entry *analysisServices.Entry) error {
|
||||
return a.sudoService.Process(entry)
|
||||
if len(errClearDB) > 0 {
|
||||
return nil, fmt.Errorf("failed to clear database data: %v", errClearDB)
|
||||
}
|
||||
|
||||
return errClearDB, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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/analyzer/log/analysis/alert_group"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/geoip"
|
||||
"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 Alert interface {
|
||||
Analyze(entry *Entry)
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type alert struct {
|
||||
rulesIndex *RulesIndex
|
||||
alertGroupService alert_group.Group
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
ipInfo geoip.Info
|
||||
}
|
||||
|
||||
type alertAnalyzeRuleReturn struct {
|
||||
found bool
|
||||
fields []*regexField
|
||||
partition *string
|
||||
}
|
||||
|
||||
type alertNotify struct {
|
||||
rule *config.AlertRule
|
||||
messages []string
|
||||
alertNumber uint64
|
||||
time time.Time
|
||||
fields []*regexField
|
||||
}
|
||||
|
||||
func NewAlert(
|
||||
rulesIndex *RulesIndex,
|
||||
alertGroupService alert_group.Group,
|
||||
logger log.Logger,
|
||||
notify notifications.Notifications,
|
||||
ipInfo geoip.Info,
|
||||
) Alert {
|
||||
return &alert{
|
||||
rulesIndex: rulesIndex,
|
||||
alertGroupService: alertGroupService,
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
ipInfo: ipInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *alert) Analyze(entry *Entry) {
|
||||
rules, err := a.rulesIndex.Alerts(entry)
|
||||
if err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to get alert rules: %s", err))
|
||||
}
|
||||
for _, rule := range rules {
|
||||
result := a.analyzeRule(rule, entry.Message)
|
||||
if !result.found {
|
||||
continue
|
||||
}
|
||||
groupName := ""
|
||||
alertNumber := uint64(0)
|
||||
messages := []string{}
|
||||
if rule.Group != nil {
|
||||
alertGroup, err := a.alertGroupService.Analyze(rule.Group, entry.Time, entry.Message, result.partition)
|
||||
if err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to analyze alert group: %s", err))
|
||||
continue
|
||||
}
|
||||
if !alertGroup.Alerted {
|
||||
continue
|
||||
}
|
||||
|
||||
groupName = rule.Group.Name
|
||||
for _, lastLog := range alertGroup.LastLogs {
|
||||
messages = append(messages, lastLog)
|
||||
}
|
||||
alertNumber = alertGroup.AlertNumber
|
||||
} else {
|
||||
messages = append(messages, entry.Message)
|
||||
}
|
||||
a.logger.Info(fmt.Sprintf("Alert detected (%s) (group:%s): %s", rule.Name, groupName, entry.Message))
|
||||
a.sendNotify(&alertNotify{
|
||||
rule: rule,
|
||||
messages: messages,
|
||||
alertNumber: alertNumber,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (a *alert) ClearDBData() error {
|
||||
return a.alertGroupService.ClearDBData()
|
||||
}
|
||||
|
||||
func (a *alert) analyzeRule(rule *config.AlertRule, message string) alertAnalyzeRuleReturn {
|
||||
result := alertAnalyzeRuleReturn{
|
||||
found: false,
|
||||
fields: []*regexField{},
|
||||
partition: nil,
|
||||
}
|
||||
|
||||
for _, pattern := range rule.Patterns {
|
||||
re, err := pattern.Regexp.Get()
|
||||
if err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to compile regexp: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
idx := re.FindStringSubmatchIndex(message)
|
||||
|
||||
if idx != nil {
|
||||
if pattern.Partition != nil {
|
||||
start, end, err := getValueStartEndByRegexIndex(int(pattern.Partition.Value), idx)
|
||||
if err != nil {
|
||||
a.logger.Error(fmt.Sprintf("alert. Failed to get partition value: %s", err))
|
||||
continue
|
||||
}
|
||||
partition := pattern.Partition.Normalize(message[start:end])
|
||||
if pattern.Partition.Type != nil {
|
||||
if accepts, partitionCode := pattern.Partition.Type.Accepts(partition); !accepts {
|
||||
continue
|
||||
} else {
|
||||
partition = partitionCode
|
||||
}
|
||||
if pattern.Partition.Type.IsPartitioned() {
|
||||
result.partition = &partition
|
||||
}
|
||||
} else {
|
||||
result.partition = &partition
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range pattern.Values {
|
||||
start, end, err := getValueStartEndByRegexIndex(int(value.Value), idx)
|
||||
if err != nil {
|
||||
result.fields = append(result.fields, ®exField{name: value.Name, value: i18n.Lang.T("unknown")})
|
||||
continue
|
||||
}
|
||||
result.fields = append(result.fields, ®exField{name: value.Name, value: message[start:end], typeValue: value.Type})
|
||||
}
|
||||
|
||||
if len(pattern.Values) != len(result.fields) {
|
||||
a.logger.Error(fmt.Sprintf("alert. analyzeRule len(pattern.Values) != len(result.fields): %d != %d", len(pattern.Values), len(result.fields)))
|
||||
result.fields = []*regexField{}
|
||||
result.partition = nil
|
||||
continue
|
||||
}
|
||||
|
||||
result.found = true
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (a *alert) sendNotify(notify *alertNotify) {
|
||||
if !notify.rule.IsNotification {
|
||||
return
|
||||
}
|
||||
|
||||
groupName := ""
|
||||
groupMessage := ""
|
||||
if notify.rule.Group != nil {
|
||||
groupName = notify.rule.Group.Name
|
||||
groupMessage = notify.rule.Group.Message + "\n\n"
|
||||
}
|
||||
|
||||
subject := i18n.Lang.T("alert.subject", map[string]any{
|
||||
"Name": notify.rule.Name,
|
||||
"GroupName": groupName,
|
||||
})
|
||||
text := subject + "\n\n" + groupMessage + notify.rule.Message + "\n\n"
|
||||
text += i18n.Lang.T("time", map[string]any{
|
||||
"Time": notify.time,
|
||||
}) + "\n"
|
||||
|
||||
for _, field := range notify.fields {
|
||||
v := field.value
|
||||
if field.typeValue == config.PatternValueIP {
|
||||
if ipInfo, err := a.ipInfo(field.value); err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to get geoip info for ip %s: %s", v, err))
|
||||
} else {
|
||||
v = ipInfo
|
||||
}
|
||||
}
|
||||
text += fmt.Sprintf("%s: %s\n", field.name, v)
|
||||
}
|
||||
if notify.alertNumber > 0 {
|
||||
text += i18n.Lang.T("alertNumber", map[string]any{
|
||||
"Count": notify.alertNumber,
|
||||
}) + "\n"
|
||||
}
|
||||
text += "\n" + i18n.Lang.T("log", map[string]any{
|
||||
"Count": len(notify.messages),
|
||||
}) + "\n"
|
||||
for _, message := range notify.messages {
|
||||
text += message + "\n\n"
|
||||
}
|
||||
a.notify.SendAsync(notifications.Message{Subject: subject, Body: text})
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package alert_group
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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/db/entity"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/repository"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/time_operation"
|
||||
)
|
||||
|
||||
type Group interface {
|
||||
Analyze(alertGroup *config.AlertGroup, eventTime time.Time, message string, partition *string) (AnalysisResult, error)
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type group struct {
|
||||
alertGroupRepository repository.AlertGroupRepository
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
type AnalysisResult struct {
|
||||
Alerted bool
|
||||
AlertNumber uint64
|
||||
LastLogs []string
|
||||
}
|
||||
|
||||
func NewGroup(alertGroupRepository repository.AlertGroupRepository, logger log.Logger) Group {
|
||||
return &group{
|
||||
alertGroupRepository: alertGroupRepository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *group) Analyze(alertGroup *config.AlertGroup, eventTime time.Time, message string, partition *string) (AnalysisResult, error) {
|
||||
analysisResult := AnalysisResult{
|
||||
Alerted: false,
|
||||
}
|
||||
|
||||
g.logger.Debug(fmt.Sprintf("Analyzing alert group %s", alertGroup.Name))
|
||||
|
||||
err := g.alertGroupRepository.Update(alertGroup.Name, partition, func(entityAlertGroup *entity.AlertGroup) (*entity.AlertGroup, error) {
|
||||
rateLimit, err := alertGroup.RateLimit(entityAlertGroup.CurrentLevelTriggerCount)
|
||||
if err != nil {
|
||||
return entityAlertGroup, err
|
||||
}
|
||||
|
||||
if time_operation.IsRateLimited(entityAlertGroup.LastTriggeredAtUnix, eventTime, int64(rateLimit.Period)) {
|
||||
g.logger.Debug(fmt.Sprintf("Alert group %s is rate limited", alertGroup.Name))
|
||||
analysisResult, entityAlertGroup = g.analysisResult(rateLimit, eventTime, message, entityAlertGroup)
|
||||
return entityAlertGroup, nil
|
||||
}
|
||||
|
||||
entityAlertGroup.TriggerCount = 0
|
||||
|
||||
if time_operation.IsReset(entityAlertGroup.LastTriggeredAtUnix, eventTime, int64(alertGroup.RateLimitResetPeriod)) {
|
||||
g.logger.Debug(fmt.Sprintf("Alert group %s is reset", alertGroup.Name))
|
||||
entityAlertGroup.Reset()
|
||||
rateLimit, err = alertGroup.RateLimit(0)
|
||||
if err != nil {
|
||||
return entityAlertGroup, err
|
||||
}
|
||||
}
|
||||
|
||||
g.logger.Debug(fmt.Sprintf("Alert not rate limited"))
|
||||
entityAlertGroup.LastLogs = []string{}
|
||||
analysisResult, entityAlertGroup = g.analysisResult(rateLimit, eventTime, message, entityAlertGroup)
|
||||
|
||||
return entityAlertGroup, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return AnalysisResult{
|
||||
Alerted: false,
|
||||
}, err
|
||||
}
|
||||
|
||||
return analysisResult, nil
|
||||
}
|
||||
|
||||
func (g *group) ClearDBData() error {
|
||||
return g.alertGroupRepository.Clear()
|
||||
}
|
||||
|
||||
func (g *group) analysisResult(rateLimit config.RateLimit, eventTime time.Time, message string, entityAlertGroup *entity.AlertGroup) (AnalysisResult, *entity.AlertGroup) {
|
||||
analysisResult := AnalysisResult{
|
||||
Alerted: false,
|
||||
}
|
||||
|
||||
entityAlertGroup.LastTriggeredAtUnix = eventTime.Unix()
|
||||
entityAlertGroup.TriggerCount++
|
||||
entityAlertGroup.LastLogs = append(entityAlertGroup.LastLogs, fmt.Sprintf("event time: %s, message: %s", eventTime.Format(time.RFC3339), message))
|
||||
g.logger.Debug(fmt.Sprintf("Alert triggered. Count: %d", entityAlertGroup.TriggerCount))
|
||||
|
||||
if entityAlertGroup.TriggerCount >= uint64(rateLimit.Count) {
|
||||
g.logger.Debug(fmt.Sprintf("Alert reached rate limit"))
|
||||
analysisResult.LastLogs = entityAlertGroup.LastLogs
|
||||
analysisResult.Alerted = true
|
||||
|
||||
entityAlertGroup.CurrentLevelTriggerCount++
|
||||
entityAlertGroup.TriggerCount = 0
|
||||
entityAlertGroup.LastLogs = []string{}
|
||||
|
||||
analysisResult.AlertNumber = entityAlertGroup.CurrentLevelTriggerCount
|
||||
} else {
|
||||
g.logger.Debug(fmt.Sprintf("Alert not reached rate limit"))
|
||||
}
|
||||
|
||||
return analysisResult, entityAlertGroup
|
||||
}
|
||||
@@ -1,29 +1,41 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
)
|
||||
|
||||
type Analysis interface {
|
||||
Process(entry *Entry) error
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
Message string
|
||||
Unit string
|
||||
PID string
|
||||
SyslogIdentifier string
|
||||
Time time.Time
|
||||
Source config.SourceType
|
||||
Message string
|
||||
Time time.Time
|
||||
|
||||
Unit string // for systemd source
|
||||
PID string // for systemd source
|
||||
SyslogIdentifier string // for systemd source
|
||||
|
||||
File string // for file source
|
||||
}
|
||||
|
||||
type processReturn struct {
|
||||
found bool
|
||||
subject string
|
||||
body string
|
||||
type regexField struct {
|
||||
name string
|
||||
value string
|
||||
typeValue config.PatternTypeValue
|
||||
}
|
||||
|
||||
type EmptyAnalysis struct{}
|
||||
func getValueStartEndByRegexIndex(valueId int, idx []int) (start int, end int, err error) {
|
||||
id := 2 * valueId
|
||||
|
||||
func (empty *EmptyAnalysis) Process(_ *Entry) error {
|
||||
return nil
|
||||
if idx == nil || len(idx) <= id+1 {
|
||||
return 0, 0, errors.New("invalid index")
|
||||
}
|
||||
|
||||
start, end = idx[id], idx[id+1]
|
||||
if start < 0 || end < 0 {
|
||||
return 0, 0, errors.New("invalid index")
|
||||
}
|
||||
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
|
||||
analysisBruteForceProtection "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection_group"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/blocking"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/geoip"
|
||||
"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/pkg/format"
|
||||
)
|
||||
|
||||
type BruteForceProtection interface {
|
||||
Analyze(entry *Entry)
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type bruteForceProtection struct {
|
||||
rulesIndex *RulesIndex
|
||||
groupService brute_force_protection_group.Group
|
||||
blockService brute_force_protection_group.BlockService
|
||||
notificationPolicy analysisBruteForceProtection.NotificationPolicy
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
ipInfo geoip.Info
|
||||
}
|
||||
|
||||
type bruteForceProtectionAnalyzeRuleReturn struct {
|
||||
found bool
|
||||
fields []*regexField
|
||||
ip net.IP
|
||||
partition *string
|
||||
}
|
||||
|
||||
type bruteForceProtectionNotify struct {
|
||||
rule *brute_force_protection.Rule
|
||||
messages []string
|
||||
blockIPCount uint64
|
||||
ip net.IP
|
||||
ports []types.L4Port
|
||||
time time.Time
|
||||
fields []*regexField
|
||||
blockSec uint32
|
||||
err error
|
||||
}
|
||||
|
||||
func NewBruteForceProtection(
|
||||
rulesIndex *RulesIndex,
|
||||
groupService brute_force_protection_group.Group,
|
||||
blockService brute_force_protection_group.BlockService,
|
||||
notificationPolicy analysisBruteForceProtection.NotificationPolicy,
|
||||
logger log.Logger,
|
||||
notify notifications.Notifications,
|
||||
ipInfo geoip.Info,
|
||||
) BruteForceProtection {
|
||||
return &bruteForceProtection{
|
||||
rulesIndex: rulesIndex,
|
||||
groupService: groupService,
|
||||
blockService: blockService,
|
||||
notificationPolicy: notificationPolicy,
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
ipInfo: ipInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) Analyze(entry *Entry) {
|
||||
rules, err := p.rulesIndex.BruteForceProtections(entry)
|
||||
if err != nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to get brute force protection rules for entry: %v", err))
|
||||
return
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if rule.Group == nil {
|
||||
p.logger.Error("Brute force protection rule without group")
|
||||
continue
|
||||
}
|
||||
|
||||
result := p.analyzeRule(rule, entry.Message)
|
||||
if !result.found {
|
||||
continue
|
||||
}
|
||||
|
||||
groupResult, err := p.groupService.Analyze(rule.Group, entry.Time, result.ip, entry.Message, result.partition)
|
||||
if err != nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to analyze brute force protection group: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if !groupResult.Block {
|
||||
continue
|
||||
}
|
||||
|
||||
ipWithPorts, l4Ports := groupResult.BlockConfig.PortsBlocked()
|
||||
if !ipWithPorts {
|
||||
p.handleBlockIP(entry, rule, &result, &groupResult)
|
||||
continue
|
||||
}
|
||||
|
||||
p.handleBlockIPWithPorts(entry, rule, &result, &groupResult, l4Ports)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) ClearDBData() error {
|
||||
return p.groupService.ClearDBData()
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) handleBlockIP(
|
||||
entry *Entry,
|
||||
rule *brute_force_protection.Rule,
|
||||
result *bruteForceProtectionAnalyzeRuleReturn,
|
||||
groupResult *brute_force_protection_group.AnalysisResult,
|
||||
) {
|
||||
blockIP := blocking.BlockIP{
|
||||
IP: result.ip,
|
||||
TimeSeconds: groupResult.BlockSec,
|
||||
Reason: rule.Message,
|
||||
}
|
||||
isBanned, err := p.blockService.BlockIP(blockIP)
|
||||
if isBanned == false {
|
||||
p.logger.Info(fmt.Sprintf("IP %s are not blocked (%s) (group:%s): %s. Err: %s", result.ip, rule.Name, rule.Group.Name, entry.Message, err.Error()))
|
||||
p.sendNotifyError(&bruteForceProtectionNotify{
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
messages: groupResult.LastLogs,
|
||||
blockIPCount: groupResult.BlockIPCount,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
p.logger.Info(fmt.Sprintf("Block IP %s detected (%s) (group:%s): %s", result.ip, rule.Name, rule.Group.Name, entry.Message))
|
||||
p.sendNotifySuccess(&bruteForceProtectionNotify{
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
messages: groupResult.LastLogs,
|
||||
blockIPCount: groupResult.BlockIPCount,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) handleBlockIPWithPorts(
|
||||
entry *Entry,
|
||||
rule *brute_force_protection.Rule,
|
||||
result *bruteForceProtectionAnalyzeRuleReturn,
|
||||
groupResult *brute_force_protection_group.AnalysisResult,
|
||||
l4Ports []types.L4Port,
|
||||
) {
|
||||
blockIPWithPorts := blocking.BlockIPWithPorts{
|
||||
IP: result.ip,
|
||||
TimeSeconds: groupResult.BlockSec,
|
||||
Reason: rule.Message,
|
||||
Ports: l4Ports,
|
||||
}
|
||||
isBanned, err := p.blockService.BlockIPWithPorts(blockIPWithPorts)
|
||||
if isBanned == false {
|
||||
p.logger.Info(fmt.Sprintf("IP %s are not blocked (%s) (group:%s): %s. Err: %s", result.ip, rule.Name, rule.Group.Name, entry.Message, err.Error()))
|
||||
p.sendNotifyError(&bruteForceProtectionNotify{
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
ports: l4Ports,
|
||||
messages: groupResult.LastLogs,
|
||||
blockIPCount: groupResult.BlockIPCount,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
p.logger.Info(fmt.Sprintf("Block IP %s detected (%s) (group:%s): %s", result.ip, rule.Name, rule.Group.Name, entry.Message))
|
||||
p.sendNotifySuccess(&bruteForceProtectionNotify{
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
ports: l4Ports,
|
||||
messages: groupResult.LastLogs,
|
||||
blockIPCount: groupResult.BlockIPCount,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) analyzeRule(rule *brute_force_protection.Rule, message string) bruteForceProtectionAnalyzeRuleReturn {
|
||||
result := bruteForceProtectionAnalyzeRuleReturn{
|
||||
found: false,
|
||||
fields: []*regexField{},
|
||||
ip: nil,
|
||||
partition: nil,
|
||||
}
|
||||
|
||||
for _, pattern := range rule.Patterns {
|
||||
re, err := pattern.Regexp.Get()
|
||||
if err != nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to compile regexp: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
idx := re.FindStringSubmatchIndex(message)
|
||||
|
||||
if idx != nil {
|
||||
start, end, err := getValueStartEndByRegexIndex(int(pattern.IP), idx)
|
||||
if err != nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to get ip value: %s", err))
|
||||
continue
|
||||
}
|
||||
ipText := message[start:end]
|
||||
ip := net.ParseIP(ipText)
|
||||
if ip == nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to parse ip: %s", ipText))
|
||||
continue
|
||||
}
|
||||
result.ip = ip
|
||||
|
||||
if pattern.Partition != nil {
|
||||
start, end, err := getValueStartEndByRegexIndex(int(pattern.Partition.Value), idx)
|
||||
if err != nil {
|
||||
p.logger.Error(fmt.Sprintf("brute_force_protection. Failed to get partition value: %s", err))
|
||||
result.ip = nil
|
||||
continue
|
||||
}
|
||||
partition := pattern.Partition.Normalize(message[start:end])
|
||||
if pattern.Partition.Type != nil {
|
||||
if accepts, partitionCode := pattern.Partition.Type.Accepts(partition); !accepts {
|
||||
continue
|
||||
} else {
|
||||
partition = partitionCode
|
||||
}
|
||||
if pattern.Partition.Type.IsPartitioned() {
|
||||
result.partition = &partition
|
||||
}
|
||||
} else {
|
||||
result.partition = &partition
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range pattern.Values {
|
||||
start, end, err := getValueStartEndByRegexIndex(int(value.Value), idx)
|
||||
if err != nil {
|
||||
result.fields = append(result.fields, ®exField{name: value.Name, value: i18n.Lang.T("unknown")})
|
||||
continue
|
||||
}
|
||||
result.fields = append(result.fields, ®exField{name: value.Name, value: message[start:end]})
|
||||
}
|
||||
|
||||
if len(pattern.Values) != len(result.fields) {
|
||||
p.logger.Error(fmt.Sprintf("brute_force_protection. analyzeRule len(pattern.Values) != len(result.fields): %d != %d", len(pattern.Values), len(result.fields)))
|
||||
result.fields = []*regexField{}
|
||||
result.partition = nil
|
||||
result.ip = nil
|
||||
continue
|
||||
}
|
||||
|
||||
result.found = true
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) sendNotifySuccess(notify *bruteForceProtectionNotify) {
|
||||
if !p.notificationPolicy.IsNotify(notify.rule) {
|
||||
return
|
||||
}
|
||||
|
||||
groupName := notify.rule.Group.Name
|
||||
|
||||
subject := i18n.Lang.T("alert.bruteForceProtection.subject", map[string]any{
|
||||
"Name": notify.rule.Name,
|
||||
"GroupName": groupName,
|
||||
"IP": notify.ip,
|
||||
})
|
||||
|
||||
p.sendNotify(subject, notify)
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) sendNotifyError(notify *bruteForceProtectionNotify) {
|
||||
if !notify.rule.IsNotification {
|
||||
return
|
||||
}
|
||||
|
||||
groupName := notify.rule.Group.Name
|
||||
|
||||
subject := i18n.Lang.T("alert.bruteForceProtection.subject-error", map[string]any{
|
||||
"Name": notify.rule.Name,
|
||||
"GroupName": groupName,
|
||||
"IP": notify.ip,
|
||||
})
|
||||
|
||||
p.sendNotify(subject, notify)
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) sendNotify(subject string, notify *bruteForceProtectionNotify) {
|
||||
if !notify.rule.IsNotification {
|
||||
return
|
||||
}
|
||||
|
||||
groupMessage := notify.rule.Group.Message + "\n\n"
|
||||
|
||||
text := subject + "\n\n" + groupMessage + notify.rule.Message + "\n\n"
|
||||
if notify.err != nil {
|
||||
text += i18n.Lang.T("alert.bruteForceProtection.error", map[string]any{
|
||||
"Error": notify.err.Error(),
|
||||
}) + "\n"
|
||||
}
|
||||
|
||||
ipInfo, err := p.ipInfo(notify.ip.String())
|
||||
if err != nil {
|
||||
ipInfo = notify.ip.String()
|
||||
p.logger.Error(fmt.Sprintf("Failed to get geoip info for ip %s: %s", notify.ip, err))
|
||||
}
|
||||
|
||||
text += "IP: " + ipInfo + "\n"
|
||||
if len(notify.ports) > 0 {
|
||||
var ports []string
|
||||
for _, port := range notify.ports {
|
||||
ports = append(ports, port.ToString())
|
||||
}
|
||||
text += i18n.Lang.T("ports", map[string]any{
|
||||
"Ports": strings.Join(ports, ", "),
|
||||
}) + "\n"
|
||||
}
|
||||
text += i18n.Lang.T("blockSec", map[string]any{
|
||||
"BlockSec": format.HumanDuration(time.Duration(notify.blockSec) * time.Second),
|
||||
}) + "\n"
|
||||
text += i18n.Lang.T("time", map[string]any{
|
||||
"Time": notify.time,
|
||||
}) + "\n"
|
||||
for _, field := range notify.fields {
|
||||
text += fmt.Sprintf("%s: %s\n", field.name, field.value)
|
||||
}
|
||||
text += i18n.Lang.T("blockIPCount", map[string]any{
|
||||
"Count": notify.blockIPCount,
|
||||
}) + "\n"
|
||||
text += "\n" + i18n.Lang.T("log", map[string]any{
|
||||
"Count": len(notify.messages),
|
||||
}) + "\n"
|
||||
for _, message := range notify.messages {
|
||||
text += message + "\n\n"
|
||||
}
|
||||
p.notify.SendAsync(notifications.Message{Subject: subject, Body: text})
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package brute_force_protection
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/repository"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type NotificationPolicy interface {
|
||||
IsNotify(rule *brute_force_protection.Rule) bool
|
||||
}
|
||||
|
||||
type notificationPolicy struct {
|
||||
notifyPolicyRepository repository.BruteForceProtectionNotifyPolicyRepository
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func NewNotificationPolicy(notifyPolicyRepository repository.BruteForceProtectionNotifyPolicyRepository, logger log.Logger) NotificationPolicy {
|
||||
return ¬ificationPolicy{
|
||||
notifyPolicyRepository: notifyPolicyRepository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *notificationPolicy) IsNotify(rule *brute_force_protection.Rule) bool {
|
||||
if !rule.IsNotification {
|
||||
return false
|
||||
}
|
||||
|
||||
if rule.NotificationCooldown == 0 && rule.NotificationEvery == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
isNotify := false
|
||||
|
||||
err := s.notifyPolicyRepository.Update(rule.Name, func(notify *entity.BruteForceProtectionNotifyPolicy) (*entity.BruteForceProtectionNotifyPolicy, error) {
|
||||
if isEvery(rule, notify) {
|
||||
isNotify = true
|
||||
return resetNotifyPolicy(notify), nil
|
||||
}
|
||||
notify.Every++
|
||||
|
||||
if rule.NotificationCooldown > 0 {
|
||||
lastNotifyAt := time.Unix(notify.CooldownTime, 0)
|
||||
cooldown := time.Duration(rule.NotificationCooldown) * time.Second
|
||||
if time.Since(lastNotifyAt) >= cooldown {
|
||||
isNotify = true
|
||||
return resetNotifyPolicy(notify), nil
|
||||
}
|
||||
}
|
||||
|
||||
return notify, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error(err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
return isNotify
|
||||
}
|
||||
|
||||
func isEvery(rule *brute_force_protection.Rule, entity *entity.BruteForceProtectionNotifyPolicy) bool {
|
||||
if rule.NotificationEvery == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if entity.Every >= rule.NotificationEvery {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func resetNotifyPolicy(entity *entity.BruteForceProtectionNotifyPolicy) *entity.BruteForceProtectionNotifyPolicy {
|
||||
entity.Every = 0
|
||||
entity.CooldownTime = time.Now().Unix()
|
||||
return entity
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package brute_force_protection_group
|
||||
|
||||
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/blocking"
|
||||
|
||||
type BlockService interface {
|
||||
BlockIP(blockIP blocking.BlockIP) (bool, error)
|
||||
BlockIPWithPorts(blockIP blocking.BlockIPWithPorts) (bool, error)
|
||||
}
|
||||
|
||||
type BlockIPFunc func(blockIP blocking.BlockIP) (bool, error)
|
||||
type BlockIPWithPortsFunc func(blockIP blocking.BlockIPWithPorts) (bool, error)
|
||||
|
||||
type blockService struct {
|
||||
blockIPFunc BlockIPFunc
|
||||
blockIPWithPortsFunc BlockIPWithPortsFunc
|
||||
}
|
||||
|
||||
func NewBlockService(blockIPFunc BlockIPFunc, blockIPWithPortsFunc BlockIPWithPortsFunc) BlockService {
|
||||
return &blockService{
|
||||
blockIPFunc: blockIPFunc,
|
||||
blockIPWithPortsFunc: blockIPWithPortsFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blockService) BlockIP(blockIP blocking.BlockIP) (bool, error) {
|
||||
return b.blockIPFunc(blockIP)
|
||||
}
|
||||
|
||||
func (b *blockService) BlockIPWithPorts(blockIP blocking.BlockIPWithPorts) (bool, error) {
|
||||
return b.blockIPWithPortsFunc(blockIP)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package brute_force_protection_group
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/repository"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/time_operation"
|
||||
)
|
||||
|
||||
type Group interface {
|
||||
Analyze(group *brute_force_protection.Group, eventTime time.Time, ip net.IP, message string, partition *string) (AnalysisResult, error)
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type group struct {
|
||||
groupRepository repository.BruteForceProtectionGroupRepository
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
type AnalysisResult struct {
|
||||
Block bool
|
||||
BlockSec uint32
|
||||
BlockConfig brute_force_protection.Block
|
||||
BlockIPCount uint64
|
||||
LastLogs []string
|
||||
}
|
||||
|
||||
func NewGroup(groupRepository repository.BruteForceProtectionGroupRepository, logger log.Logger) Group {
|
||||
return &group{
|
||||
groupRepository: groupRepository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *group) Analyze(group *brute_force_protection.Group, eventTime time.Time, ip net.IP, message string, partition *string) (AnalysisResult, error) {
|
||||
analysisResult := AnalysisResult{
|
||||
Block: false,
|
||||
}
|
||||
|
||||
g.logger.Debug(fmt.Sprintf("Analyzing brute force protection group %s IP %s", group.Name, ip.String()))
|
||||
|
||||
err := g.groupRepository.Update(group.Name, ip, partition, func(entityGroup *entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error) {
|
||||
rateLimit, err := group.RateLimit(entityGroup.CurrentLevelTriggerCount)
|
||||
if err != nil {
|
||||
return entityGroup, err
|
||||
}
|
||||
|
||||
if time_operation.IsRateLimited(entityGroup.LastTriggeredAtUnix, eventTime, int64(rateLimit.Period)) {
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection group %s is rate limited", group.Name))
|
||||
analysisResult, entityGroup = g.analysisResult(rateLimit, eventTime, message, entityGroup)
|
||||
return entityGroup, nil
|
||||
}
|
||||
|
||||
entityGroup.TriggerCount = 0
|
||||
|
||||
if time_operation.IsReset(entityGroup.LastTriggeredAtUnix, eventTime, int64(group.RateLimitResetPeriod)) {
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection group %s is reset", group.Name))
|
||||
entityGroup.Reset()
|
||||
rateLimit, err = group.RateLimit(0)
|
||||
if err != nil {
|
||||
return entityGroup, err
|
||||
}
|
||||
}
|
||||
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection not rate limited"))
|
||||
entityGroup.LastLogs = []string{}
|
||||
analysisResult, entityGroup = g.analysisResult(rateLimit, eventTime, message, entityGroup)
|
||||
|
||||
return entityGroup, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return AnalysisResult{
|
||||
Block: false,
|
||||
}, err
|
||||
}
|
||||
|
||||
return analysisResult, nil
|
||||
}
|
||||
|
||||
func (g *group) ClearDBData() error {
|
||||
return g.groupRepository.Clear()
|
||||
}
|
||||
|
||||
func (g *group) analysisResult(rateLimit brute_force_protection.RateLimit, eventTime time.Time, message string, entityGroup *entity.BruteForceProtectionGroup) (AnalysisResult, *entity.BruteForceProtectionGroup) {
|
||||
analysisResult := AnalysisResult{
|
||||
Block: false,
|
||||
}
|
||||
|
||||
entityGroup.LastTriggeredAtUnix = eventTime.Unix()
|
||||
entityGroup.TriggerCount++
|
||||
entityGroup.LastLogs = append(entityGroup.LastLogs, fmt.Sprintf("event time: %s, message: %s", eventTime.Format(time.RFC3339), message))
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection triggered. Count: %d", entityGroup.TriggerCount))
|
||||
|
||||
if entityGroup.TriggerCount >= uint64(rateLimit.Count) {
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection reached rate limit"))
|
||||
analysisResult.LastLogs = entityGroup.LastLogs
|
||||
analysisResult.Block = true
|
||||
analysisResult.BlockSec = rateLimit.BlockingTimeSeconds
|
||||
analysisResult.BlockConfig = rateLimit.BlockConfig
|
||||
|
||||
entityGroup.CurrentLevelTriggerCount++
|
||||
entityGroup.TriggerCount = 0
|
||||
entityGroup.LastLogs = []string{}
|
||||
|
||||
analysisResult.BlockIPCount = entityGroup.CurrentLevelTriggerCount
|
||||
} else {
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection not reached rate limit"))
|
||||
}
|
||||
|
||||
return analysisResult, entityGroup
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
config2 "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/analyzer/config/brute_force_protection"
|
||||
)
|
||||
|
||||
type RulesBucket interface {
|
||||
Alerts() []*config2.AlertRule
|
||||
BruteForceProtectionRules() []*brute_force_protection.Rule
|
||||
|
||||
addAlertRule(rule *config2.AlertRule)
|
||||
addBruteForceProtectionRule(rule *brute_force_protection.Rule)
|
||||
}
|
||||
|
||||
type rulesBucket struct {
|
||||
alerts []*config2.AlertRule
|
||||
bruteForceProtectionRules []*brute_force_protection.Rule
|
||||
}
|
||||
|
||||
func (rb *rulesBucket) Alerts() []*config2.AlertRule {
|
||||
return rb.alerts
|
||||
}
|
||||
|
||||
func (rb *rulesBucket) BruteForceProtectionRules() []*brute_force_protection.Rule {
|
||||
return rb.bruteForceProtectionRules
|
||||
}
|
||||
|
||||
func (rb *rulesBucket) addAlertRule(rule *config2.AlertRule) {
|
||||
rb.alerts = append(rb.alerts, rule)
|
||||
}
|
||||
|
||||
func (rb *rulesBucket) addBruteForceProtectionRule(rule *brute_force_protection.Rule) {
|
||||
rb.bruteForceProtectionRules = append(rb.bruteForceProtectionRules, rule)
|
||||
}
|
||||
|
||||
func newRulesBucket() RulesBucket {
|
||||
return &rulesBucket{
|
||||
alerts: make([]*config2.AlertRule, 0),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
config2 "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/analyzer/config/brute_force_protection"
|
||||
)
|
||||
|
||||
type RulesIndex struct {
|
||||
byKey map[indexKey]RulesBucket
|
||||
}
|
||||
|
||||
type indexKey struct {
|
||||
source config2.SourceType
|
||||
val string
|
||||
}
|
||||
|
||||
func (idx *RulesIndex) Add(source *config2.Source) error {
|
||||
if source.AlertRule == nil && source.BruteForceProtectionRule == nil {
|
||||
return fmt.Errorf("no alert rule or brute force protection rule")
|
||||
}
|
||||
|
||||
key, err := generateIndexKeyBySource(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := idx.byKey[key]; !ok {
|
||||
idx.byKey[key] = newRulesBucket()
|
||||
}
|
||||
|
||||
if source.AlertRule != nil {
|
||||
idx.byKey[key].addAlertRule(source.AlertRule)
|
||||
}
|
||||
|
||||
if source.BruteForceProtectionRule != nil {
|
||||
idx.byKey[key].addBruteForceProtectionRule(source.BruteForceProtectionRule)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (idx *RulesIndex) Alerts(entry *Entry) ([]*config2.AlertRule, error) {
|
||||
rules := make([]*config2.AlertRule, 0)
|
||||
|
||||
keys, err := generateIndexKeysByEntry(entry)
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
b, ok := idx.byKey[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
rules = append(rules, b.Alerts()...)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (idx *RulesIndex) BruteForceProtections(entry *Entry) ([]*brute_force_protection.Rule, error) {
|
||||
rules := make([]*brute_force_protection.Rule, 0)
|
||||
|
||||
keys, err := generateIndexKeysByEntry(entry)
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
b, ok := idx.byKey[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
rules = append(rules, b.BruteForceProtectionRules()...)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func NewRulesIndex() *RulesIndex {
|
||||
return &RulesIndex{byKey: make(map[indexKey]RulesBucket)}
|
||||
}
|
||||
|
||||
func generateIndexKeyBySource(source *config2.Source) (indexKey, error) {
|
||||
switch source.Type {
|
||||
case config2.SourceTypeJournal:
|
||||
match := source.Journal.JournalctlMatch()
|
||||
if source.Journal.Field == "" || source.Journal.Match == "" {
|
||||
return indexKey{}, errors.New("journalctl match is empty")
|
||||
}
|
||||
return indexKey{source: source.Type, val: match}, nil
|
||||
case config2.SourceTypeFile:
|
||||
return indexKey{source: source.Type, val: source.File.Path}, nil
|
||||
}
|
||||
|
||||
return indexKey{}, errors.New(fmt.Sprintf("unknown source type: %s", source.Type))
|
||||
}
|
||||
|
||||
func generateIndexKeysByEntry(entry *Entry) ([]indexKey, error) {
|
||||
var keys []indexKey
|
||||
|
||||
switch entry.Source {
|
||||
case config2.SourceTypeJournal:
|
||||
keys = append(keys, indexKey{source: entry.Source, val: string(config2.JournalFieldSystemdUnit) + "=" + entry.Unit})
|
||||
keys = append(keys, indexKey{source: entry.Source, val: string(config2.JournalFieldSyslogIdentifier) + "=" + entry.SyslogIdentifier})
|
||||
return keys, nil
|
||||
case config2.SourceTypeFile:
|
||||
keys = append(keys, indexKey{source: entry.Source, val: entry.File})
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
return []indexKey{}, errors.New(fmt.Sprintf("unknown source type: %s", entry.Source))
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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/analyzer/log/file_monitoring"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
"github.com/nxadm/tail"
|
||||
)
|
||||
|
||||
type FileMonitoring interface {
|
||||
Run(ctx context.Context, logChan chan<- analysisServices.Entry)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type fileMonitoring struct {
|
||||
paths []string
|
||||
logger log.Logger
|
||||
|
||||
tailers []*tail.Tail
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewFileMonitoring(paths []string, logger log.Logger) FileMonitoring {
|
||||
return &fileMonitoring{
|
||||
paths: paths,
|
||||
logger: logger,
|
||||
tailers: []*tail.Tail{},
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *fileMonitoring) Run(ctx context.Context, logChan chan<- analysisServices.Entry) {
|
||||
pathsCount := len(fm.paths)
|
||||
if pathsCount == 0 {
|
||||
fm.logger.Debug("No paths specified for file monitoring")
|
||||
return
|
||||
}
|
||||
|
||||
fm.logger.Debug("Starting file monitoring")
|
||||
|
||||
tailLogger := file_monitoring.NewLogger(fm.logger)
|
||||
|
||||
for _, path := range fm.paths {
|
||||
path := path
|
||||
go func() {
|
||||
fm.monitorFile(path, ctx, logChan, tailLogger)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *fileMonitoring) Close() error {
|
||||
for _, t := range fm.tailers {
|
||||
_ = t.Stop()
|
||||
fm.logger.Debug(fmt.Sprintf("Stopped monitoring file %s", t.Filename))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fm *fileMonitoring) monitorFile(path string, ctx context.Context, logChan chan<- analysisServices.Entry, tailLogger file_monitoring.Logger) {
|
||||
fm.logger.Debug(fmt.Sprintf("Monitoring file %s", path))
|
||||
t, err := tail.TailFile(path, tail.Config{
|
||||
Follow: true,
|
||||
ReOpen: true,
|
||||
Poll: true,
|
||||
Location: &tail.SeekInfo{Offset: 0, Whence: io.SeekEnd},
|
||||
Logger: tailLogger,
|
||||
})
|
||||
|
||||
fm.mu.Lock()
|
||||
fm.tailers = append(fm.tailers, t)
|
||||
fm.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
fm.logger.Error(fmt.Sprintf("Failed to tail file %s: %s", path, err))
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case line, ok := <-t.Lines:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if line == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
entry := analysisServices.Entry{
|
||||
Source: config.SourceTypeFile,
|
||||
File: path,
|
||||
Message: line.Text,
|
||||
Time: time.Now(),
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case logChan <- entry:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package file_monitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Fatal(v ...interface{})
|
||||
Fatalf(format string, v ...interface{})
|
||||
Fatalln(v ...interface{})
|
||||
Panic(v ...interface{})
|
||||
Panicf(format string, v ...interface{})
|
||||
Panicln(v ...interface{})
|
||||
Print(v ...interface{})
|
||||
Printf(format string, v ...interface{})
|
||||
Println(v ...interface{})
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func NewLogger(log log.Logger) Logger {
|
||||
return &logger{logger: log}
|
||||
}
|
||||
|
||||
func (l *logger) Fatal(v ...interface{}) {
|
||||
l.logger.Error(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
|
||||
func (l *logger) Fatalf(format string, v ...interface{}) {
|
||||
l.logger.Error(fmt.Sprintf("File Monitoring: "+format, v...))
|
||||
}
|
||||
|
||||
func (l *logger) Fatalln(v ...interface{}) {
|
||||
l.logger.Error(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
|
||||
func (l *logger) Panic(v ...interface{}) {
|
||||
l.logger.Error(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
|
||||
func (l *logger) Panicf(format string, v ...interface{}) {
|
||||
l.logger.Warn(fmt.Sprintf("File Monitoring: "+format, v...))
|
||||
}
|
||||
|
||||
func (l *logger) Panicln(v ...interface{}) {
|
||||
l.logger.Error(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
|
||||
func (l *logger) Print(v ...interface{}) {
|
||||
l.logger.Warn(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
|
||||
func (l *logger) Printf(format string, v ...interface{}) {
|
||||
l.logger.Warn(fmt.Sprintf("File Monitoring: "+format, v...))
|
||||
}
|
||||
|
||||
func (l *logger) Println(v ...interface{}) {
|
||||
l.logger.Warn(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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/log"
|
||||
)
|
||||
@@ -20,9 +21,9 @@ type Systemd interface {
|
||||
}
|
||||
|
||||
type systemd struct {
|
||||
path string
|
||||
units []string
|
||||
logger log.Logger
|
||||
path string
|
||||
matches []string
|
||||
logger log.Logger
|
||||
|
||||
cmd *exec.Cmd
|
||||
mu sync.Mutex
|
||||
@@ -37,17 +38,17 @@ type journalRawEntry struct {
|
||||
RealtimeTimestamp string `json:"__REALTIME_TIMESTAMP"`
|
||||
}
|
||||
|
||||
func NewSystemd(path string, units []string, logger log.Logger) Systemd {
|
||||
func NewSystemd(path string, matches []string, logger log.Logger) Systemd {
|
||||
return &systemd{
|
||||
path: path,
|
||||
units: units,
|
||||
logger: logger,
|
||||
path: path,
|
||||
matches: matches,
|
||||
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")
|
||||
if len(s.matches) == 0 {
|
||||
s.logger.Debug("No matches specified for journalctl")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -75,11 +76,11 @@ func (s *systemd) Run(ctx context.Context, logChan chan<- analysisServices.Entry
|
||||
|
||||
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 {
|
||||
for index, match := range s.matches {
|
||||
if index > 0 {
|
||||
args = append(args, "+")
|
||||
}
|
||||
args = append(args, unit)
|
||||
args = append(args, match)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, s.path, args...)
|
||||
|
||||
@@ -118,20 +119,27 @@ func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Ent
|
||||
entryTime = time.Now()
|
||||
}
|
||||
|
||||
logChan <- analysisServices.Entry{
|
||||
entry := analysisServices.Entry{
|
||||
Source: config.SourceTypeJournal,
|
||||
Message: raw.Message,
|
||||
Unit: raw.Unit,
|
||||
PID: raw.PID,
|
||||
SyslogIdentifier: raw.SyslogIdentifier,
|
||||
Time: entryTime,
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case logChan <- entry:
|
||||
}
|
||||
}
|
||||
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
func (s *systemd) Close() error {
|
||||
if s.units == nil {
|
||||
if s.matches == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
package blocklist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/repository"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft/block"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/filesystem"
|
||||
)
|
||||
|
||||
type Blocklist interface {
|
||||
Names() []string
|
||||
NftReload(blocks map[string]block.Blocklist) error
|
||||
Run()
|
||||
Close() error
|
||||
}
|
||||
|
||||
type updateSource struct {
|
||||
forcedly bool
|
||||
source *SourceConfig
|
||||
}
|
||||
|
||||
type blocklist struct {
|
||||
pathDir string
|
||||
sources []*SourceConfig
|
||||
blocklistRepository repository.BlocklistRepository
|
||||
logger log.Logger
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
|
||||
nftBlocklists map[string]block.Blocklist
|
||||
mu sync.Mutex
|
||||
|
||||
launchChannel chan updateSource
|
||||
}
|
||||
|
||||
func New(config Config, ctx context.Context, logger log.Logger) (Blocklist, error) {
|
||||
if config.PathDir == "" {
|
||||
return nil, fmt.Errorf("pathDir is empty")
|
||||
}
|
||||
|
||||
if err := filesystem.EnsureDir(config.PathDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &blocklist{
|
||||
pathDir: config.PathDir,
|
||||
sources: config.Sources,
|
||||
blocklistRepository: config.BlocklistRepository,
|
||||
logger: logger,
|
||||
ctx: ctx,
|
||||
|
||||
nftBlocklists: map[string]block.Blocklist{},
|
||||
mu: sync.Mutex{},
|
||||
|
||||
launchChannel: make(chan updateSource, 50),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *blocklist) Names() []string {
|
||||
var names []string
|
||||
for _, source := range b.sources {
|
||||
if source.Name != "" {
|
||||
names = append(names, source.Name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (b *blocklist) NftReload(blocks map[string]block.Blocklist) error {
|
||||
b.logger.Debug("Reload blocklist")
|
||||
|
||||
b.mu.Lock()
|
||||
b.nftBlocklists = blocks
|
||||
b.mu.Unlock()
|
||||
|
||||
for _, source := range b.sources {
|
||||
if nftBlocklist, ok := b.nftBlocklists[source.Name]; ok {
|
||||
if listEntity, err := b.blocklistRepository.Get(source.Name); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to get blocklist %s: %s", source.Name, err))
|
||||
} else if b.isFresh(source, listEntity) {
|
||||
file, err := b.pathFile(source)
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to get blocklist file path: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := nftBlocklist.ReplaceElementsWithFile(file); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to replace elements with file %s: %s", file, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b.logger.Error(fmt.Sprintf("NFTables sets blocklist %s not found", source.Name))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *blocklist) Run() {
|
||||
b.logger.Debug("Starting blocklist")
|
||||
if b.cancel != nil {
|
||||
// already started
|
||||
b.logger.Warn("Blocklist already started")
|
||||
return
|
||||
}
|
||||
b.ctx, b.cancel = context.WithCancel(b.ctx)
|
||||
go b.processUpdateData(b.ctx)
|
||||
|
||||
for _, src := range b.sources {
|
||||
if src == nil || src.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
interval := src.Interval
|
||||
if interval <= 0 {
|
||||
interval = 5 * time.Minute // дефолт
|
||||
}
|
||||
|
||||
b.wg.Add(1)
|
||||
go b.runSourceWorker(src, interval)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blocklist) runSourceWorker(sourceConfig *SourceConfig, interval time.Duration) {
|
||||
defer b.wg.Done()
|
||||
|
||||
b.launchChannel <- updateSource{
|
||||
forcedly: false,
|
||||
source: sourceConfig,
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.ctx.Done():
|
||||
b.logger.Debug(fmt.Sprintf("source %s stopped", sourceConfig.Name))
|
||||
return
|
||||
case <-ticker.C:
|
||||
b.logger.Debug(fmt.Sprintf("source %s tick", sourceConfig.Name))
|
||||
b.launchChannel <- updateSource{
|
||||
forcedly: true,
|
||||
source: sourceConfig,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blocklist) processUpdateData(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case updSource, ok := <-b.launchChannel:
|
||||
if !ok {
|
||||
// Channel closed
|
||||
return
|
||||
}
|
||||
|
||||
if updSource.forcedly {
|
||||
b.refreshSource(updSource.source)
|
||||
continue
|
||||
}
|
||||
|
||||
if listEntity, err := b.blocklistRepository.Get(updSource.source.Name); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to get blocklist %s: %s", updSource.source.Name, err))
|
||||
continue
|
||||
} else if b.isFresh(updSource.source, listEntity) {
|
||||
b.logger.Debug(fmt.Sprintf("blocklist %s is fresh", updSource.source.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
b.refreshSource(updSource.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blocklist) refreshSource(sourceConfig *SourceConfig) {
|
||||
ipsV4, ipsV6, err := sourceConfig.Source.Get()
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to get IPs from source %s: %s", sourceConfig.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
if nftBlocklist, ok := b.nftBlocklists[sourceConfig.Name]; ok {
|
||||
listEntity := &entity.Blocklist{
|
||||
UpdatedAtUnix: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := filesystem.EnsureDir(b.pathDir); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to ensure dir: %s", err))
|
||||
}
|
||||
file, err := b.pathFile(sourceConfig)
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to get blocklist file path: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := nftBlocklist.ReplaceElements(ipsV4, ipsV6, file); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to replace elements: %s", err))
|
||||
}
|
||||
listEntity.Checksum, err = filesystem.FileChecksum(file)
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to calculate checksum for %s: %s", file, err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := b.blocklistRepository.Update(sourceConfig.Name, listEntity); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to update blocklist %s: %s", sourceConfig.Name, err))
|
||||
}
|
||||
|
||||
} else {
|
||||
b.logger.Error(fmt.Sprintf("NFTables sets blocklist %s not found", sourceConfig.Name))
|
||||
return
|
||||
}
|
||||
|
||||
b.logger.Debug(fmt.Sprintf("refresh blocklist from %s", sourceConfig.Name))
|
||||
}
|
||||
|
||||
func (b *blocklist) Close() error {
|
||||
b.logger.Debug("Stopping blocklist")
|
||||
if b.cancel != nil {
|
||||
b.cancel()
|
||||
b.wg.Wait()
|
||||
b.cancel = nil
|
||||
}
|
||||
close(b.launchChannel)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *blocklist) pathFile(sourceConfig *SourceConfig) (string, error) {
|
||||
if sourceConfig == nil {
|
||||
return "", fmt.Errorf("sourceConfig is nil")
|
||||
}
|
||||
|
||||
if sourceConfig.Name == "" {
|
||||
return "", fmt.Errorf("sourceConfig.Name is empty")
|
||||
}
|
||||
|
||||
return strings.TrimRight(b.pathDir, "/") + "/" + sourceConfig.Name + ".nft", nil
|
||||
}
|
||||
|
||||
func (b *blocklist) isFresh(sourceConfig *SourceConfig, listEntity *entity.Blocklist) bool {
|
||||
if !listEntity.IsFresh(sourceConfig.Interval) {
|
||||
return false
|
||||
}
|
||||
|
||||
file, err := b.pathFile(sourceConfig)
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to get blocklist file path: %s", err))
|
||||
return false
|
||||
}
|
||||
if !filesystem.FileExists(file) {
|
||||
b.logger.Warn(fmt.Sprintf("Blocklist file %s not found", file))
|
||||
return false
|
||||
}
|
||||
|
||||
fileChecksum, err := filesystem.FileChecksum(file)
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to calculate checksum for %s: %s", file, err))
|
||||
return false
|
||||
}
|
||||
if listEntity.Checksum != fileChecksum {
|
||||
b.logger.Error(fmt.Sprintf("Blocklist file %s checksum is not equal to database checksum", file))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package blocklist
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/blocklist/sources"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/repository"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
BlocklistRepository repository.BlocklistRepository
|
||||
Sources []*SourceConfig
|
||||
PathDir string
|
||||
}
|
||||
|
||||
type SourceConfig struct {
|
||||
Name string
|
||||
Interval time.Duration
|
||||
Source sources.BlocklistSource
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package blocklist
|
||||
|
||||
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft/block"
|
||||
|
||||
type FalseBlocklist struct {
|
||||
}
|
||||
|
||||
func NewFalseBlocklist() Blocklist {
|
||||
return &FalseBlocklist{}
|
||||
}
|
||||
|
||||
func (b *FalseBlocklist) Names() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (b *FalseBlocklist) NftReload(_ map[string]block.Blocklist) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *FalseBlocklist) Run() {}
|
||||
|
||||
func (b *FalseBlocklist) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package sources
|
||||
|
||||
import (
|
||||
"git.kor-elf.net/kor-elf-shield/blocklist"
|
||||
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
|
||||
)
|
||||
|
||||
type BlocklistSource interface {
|
||||
Get() (ipV4 parser.IPs, ipV6 parser.IPs, err error)
|
||||
}
|
||||
|
||||
type blocklistSource struct {
|
||||
url string
|
||||
parser parser.Parser
|
||||
config blocklist.Config
|
||||
}
|
||||
|
||||
func NewBlocklistSource(url string, parser parser.Parser, config blocklist.Config) BlocklistSource {
|
||||
return &blocklistSource{
|
||||
url: url,
|
||||
parser: parser,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blocklistSource) Get() (ipV4 parser.IPs, ipV6 parser.IPs, err error) {
|
||||
return blocklist.GetSeparatedIPs(b.url, b.parser, b.config)
|
||||
}
|
||||
|
||||
type blocklistSourceZip struct {
|
||||
url string
|
||||
parser parser.Parser
|
||||
config blocklist.ConfigZip
|
||||
}
|
||||
|
||||
func NewBlocklistSourceZip(url string, parser parser.Parser, config blocklist.ConfigZip) BlocklistSource {
|
||||
return &blocklistSourceZip{
|
||||
url: url,
|
||||
parser: parser,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blocklistSourceZip) Get() (ipV4 parser.IPs, ipV6 parser.IPs, err error) {
|
||||
return blocklist.GetZipSeparatedIPs(b.url, b.parser, b.config)
|
||||
}
|
||||
+252
-10
@@ -3,15 +3,29 @@ package daemon
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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/blocklist"
|
||||
"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/firewall/blocking"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/guard"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/geoip"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/info"
|
||||
"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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/format"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
)
|
||||
|
||||
type Daemon interface {
|
||||
@@ -20,13 +34,17 @@ type Daemon interface {
|
||||
}
|
||||
|
||||
type daemon struct {
|
||||
pidFile pidfile.PidFile
|
||||
socket socket.Socket
|
||||
logger log.Logger
|
||||
firewall firewall.API
|
||||
notifications notifications.Notifications
|
||||
analyzer analyzer.Analyzer
|
||||
docker docker_monitor.Docker
|
||||
info info.Info
|
||||
pidFile pidfile.PidFile
|
||||
socket socket.Socket
|
||||
logger log.Logger
|
||||
firewall firewall.API
|
||||
firewallRulesGuard guard.RulesGuard
|
||||
notifications notifications.Notifications
|
||||
analyzer analyzer.Analyzer
|
||||
docker docker_monitor.Docker
|
||||
blocklist blocklist.Blocklist
|
||||
geoIPService geoip.GeoIP
|
||||
|
||||
stopCh chan struct{}
|
||||
}
|
||||
@@ -38,11 +56,15 @@ func (d *daemon) Run(ctx context.Context, isTesting bool, testingInterval uint16
|
||||
if err := d.socket.EnsureNoOtherProcess(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.firewall.Reload(); err != nil {
|
||||
if err := d.firewall.Reload(d.info); err != nil {
|
||||
d.firewall.ClearRules()
|
||||
return err
|
||||
}
|
||||
d.firewall.SavesRules()
|
||||
d.firewallRulesGuard.Run(d.info, ctx)
|
||||
defer func() {
|
||||
_ = d.firewallRulesGuard.Close()
|
||||
}()
|
||||
|
||||
if err := d.pidFile.Create(); err != nil {
|
||||
return err
|
||||
@@ -75,6 +97,16 @@ func (d *daemon) Run(ctx context.Context, isTesting bool, testingInterval uint16
|
||||
}()
|
||||
}
|
||||
|
||||
d.blocklist.Run()
|
||||
defer func() {
|
||||
_ = d.blocklist.Close()
|
||||
}()
|
||||
|
||||
d.geoIPService.Run(ctx)
|
||||
defer func() {
|
||||
_ = d.geoIPService.Close()
|
||||
}()
|
||||
|
||||
go d.socket.Run(ctx, d.socketCommand)
|
||||
d.runWorker(ctx, isTesting, testingInterval)
|
||||
|
||||
@@ -104,6 +136,19 @@ func (d *daemon) runWorker(ctx context.Context, isTesting bool, testingInterval
|
||||
return
|
||||
case <-stopTestingCh:
|
||||
d.logger.Info("Testing interval expired, stopping service")
|
||||
|
||||
if err := d.notifications.DBQueueClear(); err != nil {
|
||||
d.logger.Error(fmt.Sprintf("failed to clear notifications queue: %v", err))
|
||||
}
|
||||
|
||||
if err := d.analyzer.ClearDBData(); err != nil {
|
||||
d.logger.Error(fmt.Sprintf("failed to clear analyzer data: %v", err))
|
||||
}
|
||||
|
||||
if err := d.firewall.ClearDBData(); err != nil {
|
||||
d.logger.Error(fmt.Sprintf("failed to clear firewall data: %v", err))
|
||||
}
|
||||
|
||||
d.Stop()
|
||||
return
|
||||
case <-d.stopCh:
|
||||
@@ -113,21 +158,218 @@ func (d *daemon) runWorker(ctx context.Context, isTesting bool, testingInterval
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) socketCommand(command string, socket socket.Connect) error {
|
||||
func (d *daemon) socketCommand(command string, args map[string]string, socket socket.Connect) error {
|
||||
switch command {
|
||||
case "stop":
|
||||
d.stopCh <- struct{}{}
|
||||
return socket.Write("ok")
|
||||
|
||||
case "status":
|
||||
return socket.Write("ok")
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
hasRules, err := d.firewall.HasRules()
|
||||
nftables := "\033[32mOk\033[0m"
|
||||
if err != nil || !hasRules {
|
||||
nftables = "\033[31mError\033[0m"
|
||||
}
|
||||
|
||||
text := fmt.Sprintf(
|
||||
"ok\n\n***\n"+
|
||||
"NFTables: %s\n"+
|
||||
"Version: %s\n"+
|
||||
"BuiltWith: %s\n"+
|
||||
"Uptime: %s\n"+
|
||||
"Goroutines: %d\n"+
|
||||
"Alloc: %s\n"+
|
||||
"HeapAlloc: %s\n"+
|
||||
"Sys: %s\n"+
|
||||
"HeapSys: %s\n"+
|
||||
"NumGC: %d\n"+
|
||||
"***\n",
|
||||
nftables,
|
||||
d.info.Version(),
|
||||
d.info.BuiltWith(),
|
||||
format.HumanDuration(d.info.Uptime()),
|
||||
runtime.NumGoroutine(),
|
||||
format.HumanBytes(m.Alloc), // Alloc is the total bytes of allocated heap objects.
|
||||
format.HumanBytes(m.HeapAlloc), // HeapAlloc is the total bytes of heap memory obtained from the OS.
|
||||
format.HumanBytes(m.Sys), // Sys is the total bytes of memory obtained from the OS.
|
||||
format.HumanBytes(m.HeapSys), // HeapSys is the total bytes of heap memory obtained from the OS.
|
||||
m.NumGC,
|
||||
)
|
||||
return socket.Write(text)
|
||||
|
||||
case "reopen_logger":
|
||||
if err := d.logger.ReOpen(); err != nil {
|
||||
_ = socket.Write("logger reopen failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
return socket.Write("ok")
|
||||
case "notifications_queue_count":
|
||||
count := d.notifications.DBQueueSize()
|
||||
return socket.Write(strconv.Itoa(count))
|
||||
case "notifications_queue_clear":
|
||||
if err := d.notifications.DBQueueClear(); err != nil {
|
||||
_ = socket.Write("notifications queue clear failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
return socket.Write("ok")
|
||||
case "block_add_ip":
|
||||
if args["ip"] == "" {
|
||||
return socket.Write("ip argument is required")
|
||||
}
|
||||
ipAddr := net.ParseIP(args["ip"])
|
||||
if ipAddr == nil {
|
||||
_ = socket.Write("invalid ip address")
|
||||
return errors.New("invalid ip address")
|
||||
}
|
||||
|
||||
port := args["port"]
|
||||
if port != "" {
|
||||
if err := d.cmdBlockAddIPWithPort(ipAddr, port, args); err != nil {
|
||||
_ = socket.Write("block add failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := d.cmdBlockAddIP(ipAddr, args); err != nil {
|
||||
_ = socket.Write("block add failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return socket.Write("ok")
|
||||
case "block_delete_ip":
|
||||
if args["ip"] == "" {
|
||||
return socket.Write("ip argument is required")
|
||||
}
|
||||
ipAddr := net.ParseIP(args["ip"])
|
||||
if ipAddr == nil {
|
||||
_ = socket.Write("invalid ip address")
|
||||
return errors.New("invalid ip address")
|
||||
}
|
||||
|
||||
if err := d.firewall.UnblockIP(ipAddr); err != nil {
|
||||
_ = socket.Write("block delete failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return socket.Write("ok")
|
||||
|
||||
case "block_clear":
|
||||
if err := d.firewall.UnblockAllIPs(); err != nil {
|
||||
_ = socket.Write("block clear failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
return socket.Write("ok")
|
||||
|
||||
case "geoip_info":
|
||||
if args["ip"] == "" {
|
||||
return socket.Write("ip argument is required")
|
||||
}
|
||||
info, err := d.geoIPService.Info(args["ip"])
|
||||
if err != nil {
|
||||
_ = socket.Write("geoip info failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
return socket.Write(info)
|
||||
|
||||
case "geoip_refresh":
|
||||
ctx := context.Background()
|
||||
if err := d.geoIPService.Refresh(ctx); err != nil {
|
||||
_ = socket.Write("geoip refresh failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
_ = socket.Write("ok")
|
||||
return nil
|
||||
|
||||
default:
|
||||
_ = socket.Write("unknown command")
|
||||
return errors.New("unknown command")
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) cmdBlockAddIP(ip net.IP, args map[string]string) error {
|
||||
blockIP := blocking.BlockIP{
|
||||
IP: ip,
|
||||
}
|
||||
|
||||
if args["seconds"] != "" {
|
||||
seconds, err := strconv.Atoi(args["seconds"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blockIP.TimeSeconds = uint32(seconds)
|
||||
}
|
||||
|
||||
if args["reason"] != "" {
|
||||
blockIP.Reason = args["reason"]
|
||||
}
|
||||
|
||||
isBlock, err := d.firewall.BlockIP(blockIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isBlock {
|
||||
return errors.New("the IP address is not blocked")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *daemon) cmdBlockAddIPWithPort(ip net.IP, port string, args map[string]string) error {
|
||||
l4Port, err := newL4PortFromString(port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blockIP := blocking.BlockIPWithPorts{
|
||||
IP: ip,
|
||||
Ports: []types.L4Port{l4Port},
|
||||
}
|
||||
|
||||
if args["seconds"] != "" {
|
||||
seconds, err := strconv.Atoi(args["seconds"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blockIP.TimeSeconds = uint32(seconds)
|
||||
}
|
||||
|
||||
if args["reason"] != "" {
|
||||
blockIP.Reason = args["reason"]
|
||||
}
|
||||
|
||||
isBlock, err := d.firewall.BlockIPWithPorts(blockIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isBlock {
|
||||
return errors.New("the IP address is not blocked")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newL4PortFromString(s string) (types.L4Port, error) {
|
||||
if s == "" {
|
||||
return nil, errors.New("port is empty")
|
||||
}
|
||||
|
||||
data := strings.Split(s, "/")
|
||||
protocol := types.ProtocolTCP
|
||||
port, err := strconv.Atoi(data[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validate.Port(port, "port"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) == 2 {
|
||||
protocol, err = ip.ToProtocol(data[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return types.NewL4Port(uint16(port), protocol)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/repository"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/filesystem"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const (
|
||||
appDB = "app.db"
|
||||
securityDB = "security.db"
|
||||
)
|
||||
|
||||
type Repositories interface {
|
||||
NotificationsQueue() repository.NotificationsQueueRepository
|
||||
AlertGroup() repository.AlertGroupRepository
|
||||
BruteForceProtectionGroup() repository.BruteForceProtectionGroupRepository
|
||||
BruteForceProtectionNotifyPolicy() repository.BruteForceProtectionNotifyPolicyRepository
|
||||
Blocking() repository.BlockingRepository
|
||||
Blocklist() repository.BlocklistRepository
|
||||
Metadata() repository.MetadataRepository
|
||||
|
||||
Close() error
|
||||
}
|
||||
|
||||
type repositories struct {
|
||||
notificationsQueue repository.NotificationsQueueRepository
|
||||
alertGroup repository.AlertGroupRepository
|
||||
bruteForceProtectionGroup repository.BruteForceProtectionGroupRepository
|
||||
bruteForceProtectionNotifyPolicy repository.BruteForceProtectionNotifyPolicyRepository
|
||||
blocking repository.BlockingRepository
|
||||
blocklist repository.BlocklistRepository
|
||||
metadata repository.MetadataRepository
|
||||
|
||||
db []*bbolt.DB
|
||||
}
|
||||
|
||||
func New(dataDir string) (Repositories, error) {
|
||||
if dataDir == "" {
|
||||
return &repositories{}, errors.New("data directory is empty")
|
||||
}
|
||||
if dataDir[len(dataDir)-1:] != "/" {
|
||||
dataDir += "/"
|
||||
}
|
||||
|
||||
err := filesystem.EnsureDir(dataDir)
|
||||
if err != nil {
|
||||
return &repositories{}, err
|
||||
}
|
||||
|
||||
appDB, err := bbolt.Open(dataDir+appDB, 0600, &bbolt.Options{Timeout: 3 * time.Second})
|
||||
if err != nil {
|
||||
return &repositories{}, err
|
||||
}
|
||||
|
||||
securityDB, err := bbolt.Open(dataDir+securityDB, 0600, &bbolt.Options{Timeout: 3 * time.Second})
|
||||
|
||||
return &repositories{
|
||||
notificationsQueue: repository.NewNotificationsQueueRepository(appDB),
|
||||
alertGroup: repository.NewAlertGroupRepository(appDB),
|
||||
bruteForceProtectionGroup: repository.NewBruteForceProtectionGroupRepository(securityDB),
|
||||
bruteForceProtectionNotifyPolicy: repository.NewBruteForceProtectionNotifyPolicyRepository(securityDB),
|
||||
blocking: repository.NewBlockingRepository(securityDB),
|
||||
blocklist: repository.NewBlocklistRepository(securityDB),
|
||||
metadata: repository.NewMetadataRepository(appDB),
|
||||
|
||||
db: []*bbolt.DB{appDB, securityDB},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *repositories) NotificationsQueue() repository.NotificationsQueueRepository {
|
||||
return r.notificationsQueue
|
||||
}
|
||||
|
||||
func (r *repositories) AlertGroup() repository.AlertGroupRepository {
|
||||
return r.alertGroup
|
||||
}
|
||||
|
||||
func (r *repositories) BruteForceProtectionGroup() repository.BruteForceProtectionGroupRepository {
|
||||
return r.bruteForceProtectionGroup
|
||||
}
|
||||
|
||||
func (r *repositories) BruteForceProtectionNotifyPolicy() repository.BruteForceProtectionNotifyPolicyRepository {
|
||||
return r.bruteForceProtectionNotifyPolicy
|
||||
}
|
||||
|
||||
func (r *repositories) Blocking() repository.BlockingRepository {
|
||||
return r.blocking
|
||||
}
|
||||
|
||||
func (r *repositories) Blocklist() repository.BlocklistRepository {
|
||||
return r.blocklist
|
||||
}
|
||||
|
||||
func (r *repositories) Metadata() repository.MetadataRepository {
|
||||
return r.metadata
|
||||
}
|
||||
|
||||
func (r *repositories) Close() error {
|
||||
for _, db := range r.db {
|
||||
_ = db.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package entity
|
||||
|
||||
type AlertGroup struct {
|
||||
LastTriggeredAtUnix int64 `json:"LastTriggeredAtUnix"`
|
||||
TriggerCount uint64 `json:"TriggerCount"`
|
||||
CurrentLevelTriggerCount uint64 `json:"CurrentLevelTriggerCount"`
|
||||
LastLogs []string `json:"LastLogs"`
|
||||
}
|
||||
|
||||
func (g *AlertGroup) Reset() {
|
||||
g.LastTriggeredAtUnix = 0
|
||||
g.TriggerCount = 0
|
||||
g.CurrentLevelTriggerCount = 0
|
||||
g.LastLogs = []string{}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
|
||||
)
|
||||
|
||||
type Blocking struct {
|
||||
IP string `json:"IP"`
|
||||
Ports []BlockingPort
|
||||
ExpireAtUnix int64 `json:"ExpireAtUnix"`
|
||||
Reason string `json:"Reason"`
|
||||
}
|
||||
|
||||
func (b *Blocking) IsPorts() bool {
|
||||
return len(b.Ports) > 0
|
||||
}
|
||||
|
||||
func (b *Blocking) ToL4Ports() ([]types.L4Port, error) {
|
||||
if !b.IsPorts() {
|
||||
return nil, fmt.Errorf("ports is empty")
|
||||
}
|
||||
|
||||
l4Ports := make([]types.L4Port, 0, len(b.Ports))
|
||||
for _, port := range b.Ports {
|
||||
l4port, err := port.ToL4Port()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l4Ports = append(l4Ports, l4port)
|
||||
}
|
||||
|
||||
return l4Ports, nil
|
||||
}
|
||||
|
||||
type BlockingPort struct {
|
||||
Number uint16 `json:"Port"`
|
||||
Protocol string `json:"Protocol"`
|
||||
}
|
||||
|
||||
func (p *BlockingPort) ToL4Port() (types.L4Port, error) {
|
||||
if p.Protocol == "" {
|
||||
return nil, errors.New("protocol is empty")
|
||||
}
|
||||
|
||||
protocol, err := ip.ToProtocol(p.Protocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.NewL4Port(p.Number, protocol)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Blocklist struct {
|
||||
UpdatedAtUnix int64 `json:"UpdateAtUnix"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
// IsFresh returns true if the blocklist is fresh.
|
||||
func (b *Blocklist) IsFresh(interval time.Duration) bool {
|
||||
if b.Checksum == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
lastUpdate := time.Unix(b.UpdatedAtUnix, 0)
|
||||
return b.UpdatedAtUnix > 0 && time.Since(lastUpdate) <= interval
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package entity
|
||||
|
||||
type BruteForceProtectionGroup struct {
|
||||
LastTriggeredAtUnix int64 `json:"LastTriggeredAtUnix"`
|
||||
TriggerCount uint64 `json:"TriggerCount"`
|
||||
CurrentLevelTriggerCount uint64 `json:"CurrentLevelTriggerCount"`
|
||||
LastLogs []string `json:"LastLogs"`
|
||||
}
|
||||
|
||||
func (g *BruteForceProtectionGroup) Reset() {
|
||||
g.LastTriggeredAtUnix = 0
|
||||
g.TriggerCount = 0
|
||||
g.CurrentLevelTriggerCount = 0
|
||||
g.LastLogs = []string{}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package entity
|
||||
|
||||
type BruteForceProtectionNotifyPolicy struct {
|
||||
CooldownTime int64
|
||||
Every uint32
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package entity
|
||||
|
||||
const (
|
||||
MetadataKeyVersion = "Version"
|
||||
MetadataKeyFirewallFileNft = "firewall-file-nft" // checksum of the firewall file
|
||||
)
|
||||
|
||||
type Metadata struct {
|
||||
Value string `json:"Value"`
|
||||
}
|
||||
|
||||
func KeySetting(name string) string {
|
||||
return "setting-" + name
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package entity
|
||||
|
||||
type NotificationsQueue struct {
|
||||
Subject string `json:"Subject"`
|
||||
Body string `json:"Body"`
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
bboltErrors "go.etcd.io/bbolt/errors"
|
||||
)
|
||||
|
||||
type AlertGroupRepository interface {
|
||||
Update(name string, partition *string, f func(*entity.AlertGroup) (*entity.AlertGroup, error)) error
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type alertGroupRepository struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewAlertGroupRepository(appDB *bbolt.DB) AlertGroupRepository {
|
||||
return &alertGroupRepository{
|
||||
db: appDB,
|
||||
bucket: alertGroupBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *alertGroupRepository) Update(name string, partition *string, f func(*entity.AlertGroup) (*entity.AlertGroup, error)) error {
|
||||
entityAlertGroup := &entity.AlertGroup{}
|
||||
entityAlertGroup.Reset()
|
||||
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := keyGroup(name, partition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group := b.Get(key)
|
||||
if group != nil {
|
||||
err = json.Unmarshal(group, entityAlertGroup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal alert group: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
entityAlertGroup, err = f(entityAlertGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(entityAlertGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put(key, data)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *alertGroupRepository) Clear() error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
err := tx.DeleteBucket([]byte(r.bucket))
|
||||
if errors.Is(err, bboltErrors.ErrBucketNotFound) {
|
||||
// If the bucket may not exist, ignore ErrBucketNotFound
|
||||
return nil
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func keyGroup(groupID string, partition *string) ([]byte, error) {
|
||||
if len(groupID) == 0 {
|
||||
return nil, fmt.Errorf("group id cannot be empty")
|
||||
}
|
||||
|
||||
if partition == nil {
|
||||
return []byte(groupID), nil
|
||||
}
|
||||
|
||||
partitionHash := sha256.Sum256([]byte(*partition))
|
||||
|
||||
k := make([]byte, 0, len(groupID)+1+len(partitionHash))
|
||||
k = append(k, groupID...)
|
||||
k = append(k, 0x00)
|
||||
k = append(k, partitionHash[:]...)
|
||||
return k, nil
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
bboltErrors "go.etcd.io/bbolt/errors"
|
||||
)
|
||||
|
||||
type BlockingRepository interface {
|
||||
Add(blockedIP entity.Blocking) error
|
||||
List(callback func(entity.Blocking) error) error
|
||||
DeleteByIP(ip net.IP, callback func(entity.Blocking) error) error
|
||||
DeleteExpired(limit int) (int, error)
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type blocking struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewBlockingRepository(appDB *bbolt.DB) BlockingRepository {
|
||||
return &blocking{
|
||||
db: appDB,
|
||||
bucket: blockingBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *blocking) Add(blockedIP entity.Blocking) error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(blockedIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := nextKeyByExpire(bucket, uint64(blockedIP.ExpireAtUnix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(id, data)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *blocking) List(callback func(entity.Blocking) error) error {
|
||||
return r.db.View(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(r.bucket))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bucket.ForEach(func(_, v []byte) error {
|
||||
blockedIP := entity.Blocking{}
|
||||
err := json.Unmarshal(v, &blockedIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := callback(blockedIP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (r *blocking) DeleteByIP(ip net.IP, callback func(entity.Blocking) error) error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := bucket.Cursor()
|
||||
|
||||
for k, v := c.First(); k != nil; {
|
||||
blockedIP := entity.Blocking{}
|
||||
err := json.Unmarshal(v, &blockedIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedBlockedIP := net.ParseIP(blockedIP.IP)
|
||||
if parsedBlockedIP == nil || !parsedBlockedIP.Equal(ip) {
|
||||
k, v = c.Next()
|
||||
continue
|
||||
}
|
||||
|
||||
if err := callback(blockedIP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nextK, nextV := c.Next()
|
||||
if err := bucket.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
k = nextK
|
||||
v = nextV
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *blocking) DeleteExpired(limit int) (int, error) {
|
||||
if limit <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var deleted int
|
||||
err := r.db.Update(func(tx *bbolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
c := bucket.Cursor()
|
||||
deleted = 0
|
||||
|
||||
for k, v := c.First(); k != nil && deleted < limit; {
|
||||
blockedIP := entity.Blocking{}
|
||||
if err := json.Unmarshal(v, &blockedIP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if blockedIP.ExpireAtUnix <= 0 {
|
||||
k, v = c.Next()
|
||||
continue
|
||||
}
|
||||
|
||||
if blockedIP.ExpireAtUnix > now {
|
||||
// Not expired yet
|
||||
break
|
||||
}
|
||||
|
||||
nextK, nextV := c.Next()
|
||||
if err := bucket.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
deleted++
|
||||
k = nextK
|
||||
v = nextV
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return deleted, err
|
||||
}
|
||||
|
||||
func (r *blocking) Clear() error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
err := tx.DeleteBucket([]byte(r.bucket))
|
||||
if errors.Is(err, bboltErrors.ErrBucketNotFound) {
|
||||
// If the bucket may not exist, ignore ErrBucketNotFound
|
||||
return nil
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type BlocklistRepository interface {
|
||||
Get(name string) (*entity.Blocklist, error)
|
||||
Update(name string, entity *entity.Blocklist) error
|
||||
}
|
||||
|
||||
type blocklistRepository struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewBlocklistRepository(appDB *bbolt.DB) BlocklistRepository {
|
||||
return &blocklistRepository{
|
||||
db: appDB,
|
||||
bucket: blocklistBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *blocklistRepository) Get(name string) (*entity.Blocklist, error) {
|
||||
blocklistEntity := &entity.Blocklist{}
|
||||
|
||||
err := r.db.View(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(r.bucket))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := bucket.Get([]byte(name))
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, blocklistEntity)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return blocklistEntity, err
|
||||
}
|
||||
|
||||
func (r *blocklistRepository) Update(name string, blocklistEntity *entity.Blocklist) error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := []byte(name)
|
||||
|
||||
data, err := json.Marshal(blocklistEntity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put(key, data)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
bboltErrors "go.etcd.io/bbolt/errors"
|
||||
)
|
||||
|
||||
type BruteForceProtectionGroupRepository interface {
|
||||
Update(name string, ip net.IP, partition *string, f func(*entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error)) error
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type bruteForceProtectionGroupRepository struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewBruteForceProtectionGroupRepository(appDB *bbolt.DB) BruteForceProtectionGroupRepository {
|
||||
return &bruteForceProtectionGroupRepository{
|
||||
db: appDB,
|
||||
bucket: bruteForceProtectionGroupBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *bruteForceProtectionGroupRepository) Update(name string, ip net.IP, partition *string, f func(*entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error)) error {
|
||||
entityGroup := &entity.BruteForceProtectionGroup{}
|
||||
entityGroup.Reset()
|
||||
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := keyGroupIP(name, ip, partition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group := b.Get(key)
|
||||
if group != nil {
|
||||
err = json.Unmarshal(group, entityGroup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal brute force protection group: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
entityGroup, err = f(entityGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(entityGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put(key, data)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *bruteForceProtectionGroupRepository) Clear() error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
err := tx.DeleteBucket([]byte(r.bucket))
|
||||
if errors.Is(err, bboltErrors.ErrBucketNotFound) {
|
||||
// If the bucket may not exist, ignore ErrBucketNotFound
|
||||
return nil
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func keyGroupIP(groupID string, ip net.IP, partition *string) ([]byte, error) {
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("ip cannot be nil")
|
||||
}
|
||||
|
||||
if len(groupID) == 0 {
|
||||
return nil, fmt.Errorf("group id cannot be empty")
|
||||
}
|
||||
|
||||
if ip.To4() == nil && ip.To16() == nil {
|
||||
return nil, fmt.Errorf("ip is neither IPv4 nor IPv6")
|
||||
}
|
||||
|
||||
var ipAddr net.IP
|
||||
if ip.To4() != nil {
|
||||
ipAddr = ip.To4()
|
||||
} else {
|
||||
ipAddr = ip.To16()
|
||||
}
|
||||
|
||||
if partition != nil {
|
||||
partitionHash := sha256.Sum256([]byte(*partition))
|
||||
k := make([]byte, 0, len(groupID)+1+len(ipAddr)+1+len(partitionHash))
|
||||
k = append(k, groupID...)
|
||||
k = append(k, 0x00)
|
||||
k = append(k, ipAddr...)
|
||||
k = append(k, 0x00)
|
||||
k = append(k, partitionHash[:]...)
|
||||
return k, nil
|
||||
}
|
||||
|
||||
k := make([]byte, 0, len(groupID)+1+len(ipAddr))
|
||||
k = append(k, groupID...)
|
||||
k = append(k, 0x00)
|
||||
k = append(k, ipAddr...)
|
||||
return k, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type BruteForceProtectionNotifyPolicyRepository interface {
|
||||
Update(ruleName string, f func(*entity.BruteForceProtectionNotifyPolicy) (*entity.BruteForceProtectionNotifyPolicy, error)) error
|
||||
}
|
||||
|
||||
type bruteForceProtectionNotifyPolicyRepository struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewBruteForceProtectionNotifyPolicyRepository(appDB *bbolt.DB) BruteForceProtectionNotifyPolicyRepository {
|
||||
return &bruteForceProtectionNotifyPolicyRepository{
|
||||
db: appDB,
|
||||
bucket: bruteForceProtectionNotifyPolicyBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *bruteForceProtectionNotifyPolicyRepository) Update(ruleName string, f func(*entity.BruteForceProtectionNotifyPolicy) (*entity.BruteForceProtectionNotifyPolicy, error)) error {
|
||||
entityNotify := &entity.BruteForceProtectionNotifyPolicy{}
|
||||
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := []byte(ruleName)
|
||||
|
||||
notify := b.Get(key)
|
||||
if notify != nil {
|
||||
err = json.Unmarshal(notify, entityNotify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal brute force protection notify policy: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
entityNotify, err = f(entityNotify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(entityNotify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put(key, data)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type MetadataRepository interface {
|
||||
Get(name string) (*entity.Metadata, error)
|
||||
Update(name string, entity *entity.Metadata) error
|
||||
}
|
||||
|
||||
type metadataRepository struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewMetadataRepository(appDB *bbolt.DB) MetadataRepository {
|
||||
return &metadataRepository{
|
||||
db: appDB,
|
||||
bucket: metadataBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *metadataRepository) Get(name string) (*entity.Metadata, error) {
|
||||
metadataEntity := &entity.Metadata{}
|
||||
|
||||
err := r.db.View(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(r.bucket))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := bucket.Get([]byte(name))
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, metadataEntity)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return metadataEntity, err
|
||||
}
|
||||
|
||||
func (r *metadataRepository) Update(name string, entity *entity.Metadata) error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := []byte(name)
|
||||
|
||||
data, err := json.Marshal(entity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put(key, data)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
bboltErrors "go.etcd.io/bbolt/errors"
|
||||
)
|
||||
|
||||
type NotificationsQueueRepository interface {
|
||||
Add(q entity.NotificationsQueue) error
|
||||
Get(limit int) (map[string]entity.NotificationsQueue, error)
|
||||
Delete(id string) error
|
||||
|
||||
// Count - return size of notifications queue in db
|
||||
Count() (int, error)
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type notificationsQueueRepository struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewNotificationsQueueRepository(appDB *bbolt.DB) NotificationsQueueRepository {
|
||||
return ¬ificationsQueueRepository{
|
||||
db: appDB,
|
||||
bucket: notificationsQueueBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *notificationsQueueRepository) Add(q entity.NotificationsQueue) error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := nextID(bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(id, data)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *notificationsQueueRepository) Get(limit int) (map[string]entity.NotificationsQueue, error) {
|
||||
notifications := make(map[string]entity.NotificationsQueue)
|
||||
|
||||
if limit <= 0 {
|
||||
return notifications, nil
|
||||
}
|
||||
|
||||
err := r.db.View(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(r.bucket))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := bucket.Cursor()
|
||||
for k, v := c.First(); k != nil && len(notifications) < limit; k, v = c.Next() {
|
||||
var q entity.NotificationsQueue
|
||||
if err := json.Unmarshal(v, &q); err != nil {
|
||||
return err
|
||||
}
|
||||
notifications[string(k)] = q
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return notifications, err
|
||||
}
|
||||
|
||||
func (r *notificationsQueueRepository) Delete(id string) error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(r.bucket))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bucket.Delete([]byte(id))
|
||||
})
|
||||
}
|
||||
|
||||
func (r *notificationsQueueRepository) Count() (int, error) {
|
||||
count := 0
|
||||
|
||||
err := r.db.View(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(r.bucket))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
count = bucket.Stats().KeyN
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *notificationsQueueRepository) Clear() error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
err := tx.DeleteBucket([]byte(r.bucket))
|
||||
if errors.Is(err, bboltErrors.ErrBucketNotFound) {
|
||||
// If the bucket may not exist, ignore ErrBucketNotFound
|
||||
return nil
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const (
|
||||
notificationsQueueBucket = "notifications_queue"
|
||||
alertGroupBucket = "alert_group"
|
||||
bruteForceProtectionGroupBucket = "brute_force_protection_group"
|
||||
bruteForceProtectionNotifyPolicyBucket = "brute_force_protection_notify_policy"
|
||||
blockingBucket = "blocking"
|
||||
blocklistBucket = "blocklist"
|
||||
metadataBucket = "metadata"
|
||||
)
|
||||
|
||||
func nextID(b *bbolt.Bucket) ([]byte, error) {
|
||||
seq, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(key, seq)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func nextKeyByExpire(b *bbolt.Bucket, expireUnixAt uint64) ([]byte, error) {
|
||||
seq, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 0 = "forever" -> sort after any finite timestamp
|
||||
if expireUnixAt == 0 {
|
||||
expireUnixAt = math.MaxUint64
|
||||
}
|
||||
|
||||
// 8 bytes expire + 8 bytes seq
|
||||
key := make([]byte, 16)
|
||||
|
||||
// Important: BigEndian, so that sorting by bytes matches sorting by number.
|
||||
binary.BigEndian.PutUint64(key[0:8], expireUnixAt)
|
||||
binary.BigEndian.PutUint64(key[8:16], seq)
|
||||
|
||||
return key, nil
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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{}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -3,16 +3,14 @@ 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/firewall"
|
||||
"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
|
||||
NftReload(nftDocker firewall.NFTDocker) error
|
||||
Run()
|
||||
Close() error
|
||||
}
|
||||
@@ -39,12 +37,8 @@ func New(config *Config, ctx context.Context, logger log.Logger) (Docker, error)
|
||||
}, 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) NftReload(nftDocker firewall.NFTDocker) error {
|
||||
return d.ruleStrategy.Reload(nftDocker)
|
||||
}
|
||||
|
||||
func (d *docker) Run() {
|
||||
@@ -66,9 +60,3 @@ func (d *docker) Run() {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/firewall"
|
||||
)
|
||||
|
||||
type DockerNotSupport struct {
|
||||
chains chain.Chains
|
||||
}
|
||||
type dockerNotSupport struct{}
|
||||
|
||||
func NewDockerNotSupport() Docker {
|
||||
return &DockerNotSupport{
|
||||
chains: chain.NewEmptyChains(),
|
||||
}
|
||||
return &dockerNotSupport{}
|
||||
}
|
||||
|
||||
func (d *DockerNotSupport) NftReload(_ func(chain string) (nftChain.Chain, error)) error {
|
||||
func (d *dockerNotSupport) NftReload(_ firewall.NFTDocker) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DockerNotSupport) NftChains() chain.Chains {
|
||||
return d.chains
|
||||
}
|
||||
|
||||
func (d *DockerNotSupport) Run() {
|
||||
func (d *dockerNotSupport) Run() {
|
||||
|
||||
}
|
||||
|
||||
func (d *DockerNotSupport) Close() error {
|
||||
func (d *dockerNotSupport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
nftFirewall "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft/chain"
|
||||
)
|
||||
|
||||
type NFTDocker interface {
|
||||
Chains() NFTDockerChains
|
||||
NFT() nftFirewall.NFT
|
||||
}
|
||||
|
||||
type NFTDockerChains interface {
|
||||
List() []chain.Docker
|
||||
|
||||
ForwardFilter() chain.Docker
|
||||
ForwardBridge() chain.Docker
|
||||
ForwardCT() chain.Docker
|
||||
|
||||
PreroutingFilter() chain.Docker
|
||||
DockerFilter() chain.Docker
|
||||
DockerFilterFirst() chain.Docker
|
||||
DockerFilterSecond() chain.Docker
|
||||
|
||||
DockerNat() chain.Docker
|
||||
PostroutingNat() chain.Docker
|
||||
}
|
||||
|
||||
type nftDocker struct {
|
||||
chains NFTDockerChains
|
||||
nft nftFirewall.NFT
|
||||
}
|
||||
|
||||
func NewNFT(nft nftFirewall.NFT, chains NFTDockerChains) NFTDocker {
|
||||
return &nftDocker{
|
||||
chains: chains,
|
||||
nft: nft,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *nftDocker) NFT() nftFirewall.NFT {
|
||||
return n.nft
|
||||
}
|
||||
|
||||
func (n *nftDocker) Chains() NFTDockerChains {
|
||||
return n.chains
|
||||
}
|
||||
|
||||
type nftDockerChains struct {
|
||||
forwardFilter chain.Docker
|
||||
forwardBridge chain.Docker
|
||||
forwardCT chain.Docker
|
||||
|
||||
preroutingFilter chain.Docker
|
||||
dockerFilter chain.Docker
|
||||
dockerFilterFirst chain.Docker
|
||||
dockerFilterSecond chain.Docker
|
||||
|
||||
dockerNat chain.Docker
|
||||
postroutingNat chain.Docker
|
||||
}
|
||||
|
||||
func NewNFTChains(nft nftFirewall.NFT, family family.Type, table string) NFTDockerChains {
|
||||
return &nftDockerChains{
|
||||
forwardFilter: chain.NewDocker(nft.NFT(), family, table, "docker_forward_filter"),
|
||||
forwardBridge: chain.NewDocker(nft.NFT(), family, table, "docker_forward_bridge"),
|
||||
forwardCT: chain.NewDocker(nft.NFT(), family, table, "docker_forward_ct"),
|
||||
|
||||
preroutingFilter: chain.NewDocker(nft.NFT(), family, table, "docker_prerouting_filter"),
|
||||
dockerFilter: chain.NewDocker(nft.NFT(), family, table, "docker_filter"),
|
||||
dockerFilterFirst: chain.NewDocker(nft.NFT(), family, table, "docker_filter_first"),
|
||||
dockerFilterSecond: chain.NewDocker(nft.NFT(), family, table, "docker_filter_second"),
|
||||
|
||||
dockerNat: chain.NewDocker(nft.NFT(), family, table, "docker_nat"),
|
||||
postroutingNat: chain.NewDocker(nft.NFT(), family, table, "docker_postrouting_nat"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *nftDockerChains) ForwardFilter() chain.Docker {
|
||||
return n.forwardFilter
|
||||
}
|
||||
|
||||
func (n *nftDockerChains) ForwardBridge() chain.Docker {
|
||||
return n.forwardBridge
|
||||
}
|
||||
|
||||
func (n *nftDockerChains) ForwardCT() chain.Docker {
|
||||
return n.forwardCT
|
||||
}
|
||||
|
||||
func (n *nftDockerChains) PreroutingFilter() chain.Docker {
|
||||
return n.preroutingFilter
|
||||
}
|
||||
|
||||
func (n *nftDockerChains) DockerFilter() chain.Docker {
|
||||
return n.dockerFilter
|
||||
}
|
||||
|
||||
func (n *nftDockerChains) DockerFilterFirst() chain.Docker {
|
||||
return n.dockerFilterFirst
|
||||
}
|
||||
|
||||
func (n *nftDockerChains) DockerFilterSecond() chain.Docker {
|
||||
return n.dockerFilterSecond
|
||||
}
|
||||
|
||||
func (n *nftDockerChains) DockerNat() chain.Docker {
|
||||
return n.dockerNat
|
||||
}
|
||||
|
||||
func (n *nftDockerChains) PostroutingNat() chain.Docker {
|
||||
return n.postroutingNat
|
||||
}
|
||||
|
||||
func (n *nftDockerChains) List() []chain.Docker {
|
||||
return []chain.Docker{
|
||||
n.forwardFilter,
|
||||
n.forwardBridge,
|
||||
n.forwardCT,
|
||||
n.preroutingFilter,
|
||||
n.dockerFilter,
|
||||
n.dockerFilterFirst,
|
||||
n.dockerFilterSecond,
|
||||
n.dockerNat,
|
||||
n.postroutingNat,
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ func newRuleStrategy(config *Config, dockerClient client.Docker, logger log.Logg
|
||||
|
||||
switch config.RuleStrategy {
|
||||
case RuleStrategyRebuild:
|
||||
return rule_strategy.NewRebuildStrategy(generate), nil
|
||||
return rule_strategy.NewRebuildStrategy(generate, logger), nil
|
||||
case RuleStrategyIncremental:
|
||||
return rule_strategy.NewIncrementalStrategy(generate, dockerClient, logger), nil
|
||||
}
|
||||
|
||||
@@ -3,17 +3,19 @@ package rule_strategy
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/chain"
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client/contract"
|
||||
"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/firewall"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft/chain"
|
||||
"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)
|
||||
GenerateAll(builder nft.BatchBuilder, chains firewall.NFTDockerChains, isComment bool)
|
||||
GenerateBridge(bridge client.Bridge, builder nft.BatchBuilder, chains firewall.NFTDockerChains, isComment bool)
|
||||
GenerateContainer(container client.Container, bridgeName string, builder nft.BatchBuilder, chains firewall.NFTDockerChains, isComment bool)
|
||||
ClearChains(builder nft.BatchBuilder, chains firewall.NFTDockerChains)
|
||||
AddRule(builder nft.BatchBuilder, chainDocker chain.Docker, rule string)
|
||||
}
|
||||
|
||||
type generator struct {
|
||||
@@ -28,19 +30,17 @@ func NewGenerator(dockerClient client.Docker, logger log.Logger) Generator {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) GenerateAll(chains chain.Chains, isComment bool) {
|
||||
listChains := chains.List()
|
||||
|
||||
if err := listChains.ForwardCT.JumpTo(&listChains.ForwardFilter, "", ""); err != nil {
|
||||
func (g *generator) GenerateAll(builder nft.BatchBuilder, chains firewall.NFTDockerChains, isComment bool) {
|
||||
if err := chains.ForwardCT().JumpTo(builder, chains.ForwardFilter(), "", ""); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.ForwardBridge.JumpTo(&listChains.ForwardFilter, "", ""); err != nil {
|
||||
if err := chains.ForwardBridge().JumpTo(builder, chains.ForwardFilter(), "", ""); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.DockerFilterFirst.JumpTo(&listChains.DockerFilter, "", ""); err != nil {
|
||||
if err := chains.DockerFilterFirst().JumpTo(builder, chains.DockerFilter(), "", ""); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
if err := listChains.DockerFilterSecond.JumpTo(&listChains.DockerFilter, "", ""); err != nil {
|
||||
if err := chains.DockerFilterSecond().JumpTo(builder, chains.DockerFilter(), "", ""); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
@@ -51,20 +51,18 @@ func (g *generator) GenerateAll(chains chain.Chains, isComment bool) {
|
||||
}
|
||||
|
||||
for _, bridge := range bridges {
|
||||
g.GenerateBridge(bridge, chains, isComment)
|
||||
g.GenerateBridge(bridge, builder, chains, isComment)
|
||||
|
||||
if bridge.Containers == nil {
|
||||
continue
|
||||
}
|
||||
for _, container := range bridge.Containers {
|
||||
g.GenerateContainer(container, bridge.Name, chains, isComment)
|
||||
g.GenerateContainer(container, bridge.Name, builder, chains, isComment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) GenerateBridge(bridge client.Bridge, chain chain.Chains, isComment bool) {
|
||||
listChains := chain.List()
|
||||
|
||||
func (g *generator) GenerateBridge(bridge client.Bridge, builder nft.BatchBuilder, chains firewall.NFTDockerChains, isComment bool) {
|
||||
var rule string
|
||||
comment := ""
|
||||
if isComment {
|
||||
@@ -72,27 +70,26 @@ func (g *generator) GenerateBridge(bridge client.Bridge, chain chain.Chains, isC
|
||||
}
|
||||
|
||||
rule = fmt.Sprintf("iifname != \"%s\" oifname \"%s\" counter drop %s", bridge.Name, bridge.Name, comment)
|
||||
g.AddRule(listChains.DockerFilterSecond, rule)
|
||||
g.AddRule(builder, chains.DockerFilterSecond(), rule)
|
||||
|
||||
rule = fmt.Sprintf("iifname \"%s\" counter accept %s", bridge.Name, comment)
|
||||
g.AddRule(listChains.ForwardFilter, rule)
|
||||
g.AddRule(builder, chains.ForwardFilter(), rule)
|
||||
|
||||
rule = fmt.Sprintf("oifname \"%s\" counter", bridge.Name)
|
||||
if err := listChains.DockerFilter.JumpTo(&listChains.ForwardBridge, rule, comment); err != nil {
|
||||
if err := chains.DockerFilter().JumpTo(builder, chains.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)
|
||||
g.AddRule(builder, chains.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)
|
||||
g.AddRule(builder, chains.PostroutingNat(), rule)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) GenerateContainer(container client.Container, bridgeName string, chain chain.Chains, isComment bool) {
|
||||
listChains := chain.List()
|
||||
func (g *generator) GenerateContainer(container client.Container, bridgeName string, builder nft.BatchBuilder, chains firewall.NFTDockerChains, isComment bool) {
|
||||
var rule string
|
||||
comment := ""
|
||||
if isComment {
|
||||
@@ -101,14 +98,14 @@ func (g *generator) GenerateContainer(container client.Container, bridgeName str
|
||||
|
||||
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)
|
||||
g.AddRule(builder, chains.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)
|
||||
g.AddRule(builder, chains.PreroutingFilter(), rule)
|
||||
}
|
||||
|
||||
if hostInfo.IP.Address == "0.0.0.0" || hostInfo.IP.Address == "::" {
|
||||
@@ -117,58 +114,32 @@ func (g *generator) GenerateContainer(container client.Container, bridgeName str
|
||||
}
|
||||
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)
|
||||
g.AddRule(builder, chains.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)
|
||||
g.AddRule(builder, chains.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)
|
||||
g.AddRule(builder, chains.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)
|
||||
g.AddRule(builder, chains.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) ClearChains(builder nft.BatchBuilder, chains firewall.NFTDockerChains) {
|
||||
for _, chain := range chains.List() {
|
||||
if err := chain.Clear(builder); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) AddRule(chainData chain.Data, rule string) {
|
||||
if err := chainData.AddRule(rule); err != nil {
|
||||
func (g *generator) AddRule(builder nft.BatchBuilder, chainDocker chain.Docker, rule string) {
|
||||
if err := chainDocker.AddRule(builder, rule); err != nil {
|
||||
g.logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/firewall"
|
||||
)
|
||||
|
||||
type Strategy interface {
|
||||
Reload(newNoneChain func(chain string) (nftChain.Chain, error)) error
|
||||
Chains() chain.Chains
|
||||
Reload(nftDocker firewall.NFTDocker) error
|
||||
Chains() firewall.NFTDockerChains
|
||||
Event(event *client.Event)
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@ package rule_strategy
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/chain"
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client/contract"
|
||||
"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/daemon/docker_monitor/firewall"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft/chain"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type incrementalStrategy struct {
|
||||
dockerClient client.Docker
|
||||
chains chain.Chains
|
||||
nftDocker firewall.NFTDocker
|
||||
generator Generator
|
||||
logger log.Logger
|
||||
}
|
||||
@@ -24,20 +25,26 @@ func NewIncrementalStrategy(generator Generator, dockerClient client.Docker, log
|
||||
}
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) Reload(newNoneChain func(chain string) (nftChain.Chain, error)) error {
|
||||
chains, err := chain.NewChains(newNoneChain)
|
||||
func (i *incrementalStrategy) Reload(nftDocker firewall.NFTDocker) error {
|
||||
i.nftDocker = nftDocker
|
||||
|
||||
batchBuilder, err := i.nftDocker.NFT().NewBuildBatch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.chains = chains
|
||||
defer func() {
|
||||
if err := batchBuilder.Close(); err != nil {
|
||||
i.logger.Warn(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
i.generator.GenerateAll(i.chains, true)
|
||||
i.generator.GenerateAll(batchBuilder, i.nftDocker.Chains(), true)
|
||||
|
||||
return nil
|
||||
return i.nftDocker.NFT().RunBatch(batchBuilder)
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) Chains() chain.Chains {
|
||||
return i.chains
|
||||
func (i *incrementalStrategy) Chains() firewall.NFTDockerChains {
|
||||
return i.nftDocker.Chains()
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) Event(event *client.Event) {
|
||||
@@ -54,7 +61,9 @@ func (i *incrementalStrategy) Event(event *client.Event) {
|
||||
}
|
||||
|
||||
if event.Action == "die" {
|
||||
i.eventContainerStop(event.ID)
|
||||
if err := i.eventContainerStop(event.ID); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to handle container stop event: %s", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,10 +75,14 @@ func (i *incrementalStrategy) Event(event *client.Event) {
|
||||
if err := i.eventNetworkCreate(event.ID); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to handle network create event: %s", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if event.Action == "destroy" {
|
||||
i.eventNetworkDestroy(event.ID)
|
||||
if err := i.eventNetworkDestroy(event.ID); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to handle network destroy event: %s", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
@@ -82,35 +95,55 @@ func (i *incrementalStrategy) eventContainerStart(containerId string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
batchBuilder, err := i.nftDocker.NFT().NewBuildBatch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := batchBuilder.Close(); err != nil {
|
||||
i.logger.Warn(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
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)
|
||||
i.generator.GenerateContainer(container, bridge.Name, batchBuilder, i.nftDocker.Chains(), true)
|
||||
}
|
||||
|
||||
return nil
|
||||
return i.nftDocker.NFT().RunBatch(batchBuilder)
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) eventContainerStop(containerId string) {
|
||||
listChains := i.chains.List()
|
||||
func (i *incrementalStrategy) eventContainerStop(containerId string) error {
|
||||
batchBuilder, err := i.nftDocker.NFT().NewBuildBatch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := batchBuilder.Close(); err != nil {
|
||||
i.logger.Warn(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
if err := i.nftRuleDeleteContainer(containerId, &listChains.PreroutingFilter); err != nil {
|
||||
if err := i.nftRuleDeleteContainer(containerId, batchBuilder, i.nftDocker.Chains().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 {
|
||||
if err := i.nftRuleDeleteContainer(containerId, batchBuilder, i.nftDocker.Chains().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 {
|
||||
if err := i.nftRuleDeleteContainer(containerId, batchBuilder, i.nftDocker.Chains().DockerFilterFirst()); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete container %s rules: %s", containerId, err))
|
||||
}
|
||||
|
||||
return i.nftDocker.NFT().RunBatch(batchBuilder)
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) nftRuleDeleteContainer(containerId string, chain *chain.Data) error {
|
||||
func (i *incrementalStrategy) nftRuleDeleteContainer(containerId string, builder nft.BatchBuilder, chain chain.Docker) error {
|
||||
rules, err := chain.ListRules()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -120,7 +153,7 @@ func (i *incrementalStrategy) nftRuleDeleteContainer(containerId string, chain *
|
||||
if rule.Comment != "container_id:"+containerId {
|
||||
continue
|
||||
}
|
||||
if err := chain.RemoveRuleByHandle(rule.Handle); err != nil {
|
||||
if err := chain.RemoveRuleByHandle(builder, rule.Handle); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete container %s rule: %s", containerId, err))
|
||||
}
|
||||
}
|
||||
@@ -129,40 +162,60 @@ func (i *incrementalStrategy) nftRuleDeleteContainer(containerId string, chain *
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) eventNetworkCreate(bridgeId string) error {
|
||||
batchBuilder, err := i.nftDocker.NFT().NewBuildBatch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := batchBuilder.Close(); err != nil {
|
||||
i.logger.Warn(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
bridge, err := i.dockerClient.FetchBridge(bridgeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.generator.GenerateBridge(bridge, i.chains, true)
|
||||
return nil
|
||||
i.generator.GenerateBridge(bridge, batchBuilder, i.nftDocker.Chains(), true)
|
||||
return i.nftDocker.NFT().RunBatch(batchBuilder)
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) eventNetworkDestroy(bridgeId string) {
|
||||
listChains := i.chains.List()
|
||||
func (i *incrementalStrategy) eventNetworkDestroy(bridgeId string) error {
|
||||
batchBuilder, err := i.nftDocker.NFT().NewBuildBatch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := batchBuilder.Close(); err != nil {
|
||||
i.logger.Warn(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.DockerFilterSecond); err != nil {
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, batchBuilder, i.nftDocker.Chains().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 {
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, batchBuilder, i.nftDocker.Chains().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 {
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, batchBuilder, i.nftDocker.Chains().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 {
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, batchBuilder, i.nftDocker.Chains().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 {
|
||||
if err := i.nftRuleDeleteBridge(bridgeId, batchBuilder, i.nftDocker.Chains().PostroutingNat()); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
|
||||
}
|
||||
|
||||
return i.nftDocker.NFT().RunBatch(batchBuilder)
|
||||
}
|
||||
|
||||
func (i *incrementalStrategy) nftRuleDeleteBridge(bridgeId string, chain *chain.Data) error {
|
||||
func (i *incrementalStrategy) nftRuleDeleteBridge(bridgeId string, builder nft.BatchBuilder, chain chain.Docker) error {
|
||||
rules, err := chain.ListRules()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -172,7 +225,7 @@ func (i *incrementalStrategy) nftRuleDeleteBridge(bridgeId string, chain *chain.
|
||||
if rule.Comment != "bridge_id:"+bridgeId {
|
||||
continue
|
||||
}
|
||||
if err := chain.RemoveRuleByHandle(rule.Handle); err != nil {
|
||||
if err := chain.RemoveRuleByHandle(builder, rule.Handle); err != nil {
|
||||
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rule: %s", bridgeId, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/firewall"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type rebuildStrategy struct {
|
||||
chains chain.Chains
|
||||
nftDocker firewall.NFTDocker
|
||||
generator Generator
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func NewRebuildStrategy(generator Generator) Strategy {
|
||||
func NewRebuildStrategy(generator Generator, logger log.Logger) Strategy {
|
||||
return &rebuildStrategy{
|
||||
generator: generator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rebuildStrategy) Reload(newNoneChain func(chain string) (nftChain.Chain, error)) error {
|
||||
chains, err := chain.NewChains(newNoneChain)
|
||||
func (r *rebuildStrategy) Reload(nftDocker firewall.NFTDocker) error {
|
||||
r.nftDocker = nftDocker
|
||||
|
||||
batchBuilder, err := r.nftDocker.NFT().NewBuildBatch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.chains = chains
|
||||
defer func() {
|
||||
if err := batchBuilder.Close(); err != nil {
|
||||
r.logger.Warn(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
r.generator.GenerateAll(r.chains, false)
|
||||
r.generator.GenerateAll(batchBuilder, r.nftDocker.Chains(), false)
|
||||
|
||||
return nil
|
||||
return r.nftDocker.NFT().RunBatch(batchBuilder)
|
||||
}
|
||||
|
||||
func (r *rebuildStrategy) Chains() chain.Chains {
|
||||
return r.chains
|
||||
func (r *rebuildStrategy) Chains() firewall.NFTDockerChains {
|
||||
return r.nftDocker.Chains()
|
||||
}
|
||||
|
||||
func (r *rebuildStrategy) Event(event *client.Event) {
|
||||
@@ -38,6 +46,21 @@ func (r *rebuildStrategy) Event(event *client.Event) {
|
||||
return
|
||||
}
|
||||
|
||||
r.generator.ClearChains(r.chains)
|
||||
r.generator.GenerateAll(r.chains, false)
|
||||
batchBuilder, err := r.nftDocker.NFT().NewBuildBatch()
|
||||
if err != nil {
|
||||
r.logger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := batchBuilder.Close(); err != nil {
|
||||
r.logger.Warn(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
r.generator.ClearChains(batchBuilder, r.nftDocker.Chains())
|
||||
r.generator.GenerateAll(batchBuilder, r.nftDocker.Chains(), false)
|
||||
|
||||
if err := r.nftDocker.NFT().RunBatch(batchBuilder); err != nil {
|
||||
r.logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
package blocking
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/repository"
|
||||
nftFirewall "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft/block"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
NftReload(nft nftFirewall.NFT, blockListIP block.ListIP, blockListIPWithPort block.ListIPWithPort) error
|
||||
BlockIP(block BlockIP) (bool, error)
|
||||
BlockIPWithPorts(block BlockIPWithPorts) (bool, error)
|
||||
UnblockAllIPs() error
|
||||
UnblockIP(ip net.IP) error
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type blocking struct {
|
||||
blockingRepository repository.BlockingRepository
|
||||
blockListIP block.ListIP
|
||||
blockListIPWithPort block.ListIPWithPort
|
||||
logger log.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type BlockIP struct {
|
||||
IP net.IP
|
||||
TimeSeconds uint32
|
||||
Reason string
|
||||
}
|
||||
|
||||
type BlockIPWithPorts struct {
|
||||
IP net.IP
|
||||
TimeSeconds uint32
|
||||
Reason string
|
||||
Ports []types.L4Port
|
||||
}
|
||||
|
||||
func New(blockingRepository repository.BlockingRepository, logger log.Logger) API {
|
||||
return &blocking{
|
||||
blockingRepository: blockingRepository,
|
||||
logger: logger,
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blocking) NftReload(nft nftFirewall.NFT, blockListIP block.ListIP, blockListIPWithPort block.ListIPWithPort) error {
|
||||
b.mu.Lock()
|
||||
b.blockListIP = blockListIP
|
||||
b.blockListIPWithPort = blockListIPWithPort
|
||||
b.mu.Unlock()
|
||||
|
||||
batchBuilder, err := nft.NewBuildBatch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := batchBuilder.Close(); err != nil {
|
||||
b.logger.Warn(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
isExpiredEntries := false
|
||||
nowUnix := time.Now().Unix()
|
||||
err = b.blockingRepository.List(func(e entity.Blocking) error {
|
||||
ip := net.ParseIP(e.IP)
|
||||
if ip == nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to parse IP address: %s", e.IP))
|
||||
return nil
|
||||
}
|
||||
|
||||
blockSeconds := uint32(0)
|
||||
if e.ExpireAtUnix > 0 {
|
||||
if e.ExpireAtUnix < nowUnix {
|
||||
isExpiredEntries = true
|
||||
return nil
|
||||
}
|
||||
blockSeconds = uint32(e.ExpireAtUnix - nowUnix)
|
||||
}
|
||||
|
||||
if e.IsPorts() {
|
||||
l4Ports, err := e.ToL4Ports()
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to parse ports: %s", err))
|
||||
return nil
|
||||
}
|
||||
if err := b.blockListIPWithPort.AddBatchIP(batchBuilder, ip, l4Ports, blockSeconds); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to add IP %s to block list: %s", ip.String(), err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := b.blockListIP.AddBatchIP(batchBuilder, ip, blockSeconds); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to add IP %s to block list: %s", ip.String(), err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if isExpiredEntries {
|
||||
go func() {
|
||||
deleteCount, err := b.blockingRepository.DeleteExpired(100)
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to delete expired entries from database: %s", err))
|
||||
}
|
||||
b.logger.Debug(fmt.Sprintf("Deleted %d expired entries from database", deleteCount))
|
||||
}()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nft.RunBatch(batchBuilder)
|
||||
}
|
||||
|
||||
func (b *blocking) BlockIP(block BlockIP) (bool, error) {
|
||||
if block.IP.IsLoopback() {
|
||||
return false, fmt.Errorf("loopback IP address %s cannot be blocked", block.IP.String())
|
||||
}
|
||||
|
||||
if err := b.blockListIP.AddIP(block.IP, block.TimeSeconds); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
expireAtUnix := int64(0)
|
||||
if block.TimeSeconds > 0 {
|
||||
expire := time.Now().Add(time.Duration(int64(block.TimeSeconds)) * time.Second)
|
||||
expireAtUnix = expire.Unix()
|
||||
}
|
||||
data := entity.Blocking{
|
||||
IP: block.IP.String(),
|
||||
ExpireAtUnix: expireAtUnix,
|
||||
Reason: block.Reason,
|
||||
}
|
||||
if err := b.blockingRepository.Add(data); err != nil {
|
||||
return true, fmt.Errorf("the IP is blocked, but not recorded in the database. Failed to add IP %s to database: %w", block.IP.String(), err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *blocking) BlockIPWithPorts(block BlockIPWithPorts) (bool, error) {
|
||||
if block.IP.IsLoopback() {
|
||||
return false, fmt.Errorf("loopback IP address %s cannot be blocked", block.IP.String())
|
||||
}
|
||||
|
||||
if err := b.blockListIPWithPort.AddIP(block.IP, block.Ports, block.TimeSeconds); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var l4Ports []entity.BlockingPort
|
||||
for _, port := range block.Ports {
|
||||
l4Ports = append(l4Ports, entity.BlockingPort{
|
||||
Number: port.Number(),
|
||||
Protocol: port.ProtocolString(),
|
||||
})
|
||||
}
|
||||
|
||||
expireAtUnix := int64(0)
|
||||
if block.TimeSeconds > 0 {
|
||||
expire := time.Now().Add(time.Duration(int64(block.TimeSeconds)) * time.Second)
|
||||
expireAtUnix = expire.Unix()
|
||||
}
|
||||
data := entity.Blocking{
|
||||
IP: block.IP.String(),
|
||||
ExpireAtUnix: expireAtUnix,
|
||||
Reason: block.Reason,
|
||||
Ports: l4Ports,
|
||||
}
|
||||
if err := b.blockingRepository.Add(data); err != nil {
|
||||
return true, fmt.Errorf("the IP is blocked, but not recorded in the database. Failed to add IP %s to database: %w", block.IP.String(), err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *blocking) UnblockIP(ip net.IP) error {
|
||||
err := b.blockingRepository.DeleteByIP(ip, func(e entity.Blocking) error {
|
||||
if e.IsPorts() {
|
||||
l4Ports, err := e.ToL4Ports()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.removeIPWithPorts(ip, l4Ports)
|
||||
}
|
||||
|
||||
if err := b.blockListIP.DeleteIP(ip); err != nil {
|
||||
if strings.Contains(err.Error(), "element does not exist") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *blocking) UnblockAllIPs() error {
|
||||
err := b.blockingRepository.List(func(e entity.Blocking) error {
|
||||
ip := net.ParseIP(e.IP)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("failed to parse IP address: %s", e.IP)
|
||||
}
|
||||
|
||||
if e.IsPorts() {
|
||||
l4Ports, err := e.ToL4Ports()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, port := range l4Ports {
|
||||
if err := b.blockListIPWithPort.DeleteIP(ip, port); err != nil {
|
||||
if strings.Contains(err.Error(), "element does not exist") ||
|
||||
strings.Contains(err.Error(), "Error: Could not process rule: No such file or directory") {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.blockListIP.DeleteIP(ip); err != nil {
|
||||
if strings.Contains(err.Error(), "element does not exist") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
_ = b.blockingRepository.Clear()
|
||||
return err
|
||||
}
|
||||
|
||||
return b.blockingRepository.Clear()
|
||||
}
|
||||
|
||||
func (b *blocking) ClearDBData() error {
|
||||
return b.blockingRepository.Clear()
|
||||
}
|
||||
|
||||
func (b *blocking) removeIPWithPorts(ip net.IP, l4Ports []types.L4Port) error {
|
||||
for _, port := range l4Ports {
|
||||
if err := b.blockListIPWithPort.DeleteIP(ip, port); err != nil {
|
||||
if strings.Contains(err.Error(), "element does not exist") ||
|
||||
strings.Contains(err.Error(), "Error: Could not process rule: No such file or directory") {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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,196 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type Chains interface {
|
||||
NewPacketFilter(enable bool) error
|
||||
PacketFilter() PacketFilter
|
||||
|
||||
NewInput(chain string, defaultAllow bool, priority int) error
|
||||
Input() Input
|
||||
|
||||
NewOutput(chain string, defaultAllow bool, priority int) error
|
||||
Output() Output
|
||||
|
||||
NewForward(chain string, defaultAllow bool, priority int) error
|
||||
Forward() Forward
|
||||
|
||||
NewLocalInput() error
|
||||
LocalInput() LocalInput
|
||||
|
||||
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 {
|
||||
input Input
|
||||
output Output
|
||||
forward Forward
|
||||
packetFilter PacketFilter
|
||||
|
||||
localInput LocalInput
|
||||
localOutput LocalOutput
|
||||
localForward LocalForward
|
||||
|
||||
family nftFamily.Type
|
||||
table string
|
||||
nft nft.NFT
|
||||
}
|
||||
|
||||
func NewChains(nft nft.NFT, table string) (Chains, error) {
|
||||
family := nftFamily.INET
|
||||
|
||||
if err := clearRules(nft, family, table); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Table().Add(family, table); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chains{
|
||||
nft: nft,
|
||||
table: table,
|
||||
family: family,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *chains) NewPacketFilter(enable bool) error {
|
||||
filter, err := newPacketFilter(c.nft, c.family, c.table, enable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.packetFilter = filter
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chains) PacketFilter() PacketFilter {
|
||||
return c.packetFilter
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.input = input
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chains) Input() Input {
|
||||
return c.input
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.output = output
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chains) Output() Output {
|
||||
return c.output
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.forward = forward
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chains) Forward() Forward {
|
||||
return c.forward
|
||||
}
|
||||
|
||||
func (c *chains) NewLocalInput() error {
|
||||
localInput, err := newLocalInput(c.nft, c.family, c.table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.localInput = localInput
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chains) LocalInput() LocalInput {
|
||||
return c.localInput
|
||||
}
|
||||
|
||||
func (c *chains) NewLocalOutput() error {
|
||||
localOutput, err := newLocalOutput(c.nft, c.family, c.table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.localOutput = localOutput
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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 Forward interface {
|
||||
AddRule(expr ...string) error
|
||||
}
|
||||
|
||||
type forward struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
chain string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
baseChain := nftChain.BaseChainOptions{
|
||||
Type: nftChain.TypeFilter,
|
||||
Hook: nftChain.HookForward,
|
||||
Priority: int32(priority),
|
||||
Policy: policy,
|
||||
Device: "",
|
||||
}
|
||||
|
||||
if err := nft.Chain().Add(family, table, chain, baseChain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &forward{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
chain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *forward) AddRule(expr ...string) error {
|
||||
return c.nft.Rule().Add(c.family, c.table, c.chain, expr...)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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 Input interface {
|
||||
AddRule(expr ...string) error
|
||||
}
|
||||
|
||||
type input struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
chain string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
baseChain := nftChain.BaseChainOptions{
|
||||
Type: nftChain.TypeFilter,
|
||||
Hook: nftChain.HookInput,
|
||||
Priority: int32(priority),
|
||||
Policy: policy,
|
||||
Device: "",
|
||||
}
|
||||
|
||||
if err := nft.Chain().Add(family, table, chain, baseChain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &input{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
chain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *input) AddRule(expr ...string) error {
|
||||
return c.nft.Rule().Add(c.family, c.table, c.chain, expr...)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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 LocalInput interface {
|
||||
AddRule(expr ...string) error
|
||||
AddRuleIn(AddRuleFunc func(expr ...string) error) error
|
||||
}
|
||||
|
||||
type localInput struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
chain string
|
||||
}
|
||||
|
||||
func newLocalInput(nft nft.NFT, family family.Type, table string) (LocalInput, error) {
|
||||
chain := "local-input"
|
||||
if err := nft.Chain().Add(family, table, chain, nftChain.TypeNone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &localInput{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
chain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *localInput) AddRule(expr ...string) error {
|
||||
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
|
||||
}
|
||||
|
||||
func (l *localInput) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
|
||||
return AddRuleFunc("iifname != \"lo\" counter jump " + l.chain)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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 LocalOutput interface {
|
||||
AddRule(expr ...string) error
|
||||
AddRuleOut(AddRuleFunc func(expr ...string) error) error
|
||||
}
|
||||
|
||||
type localOutput struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
chain string
|
||||
}
|
||||
|
||||
func newLocalOutput(nft nft.NFT, family family.Type, table string) (LocalOutput, error) {
|
||||
chain := "local-output"
|
||||
if err := nft.Chain().Add(family, table, chain, nftChain.TypeNone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &localOutput{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
chain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *localOutput) AddRule(expr ...string) error {
|
||||
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
|
||||
}
|
||||
|
||||
func (l *localOutput) AddRuleOut(AddRuleFunc func(expr ...string) error) error {
|
||||
return AddRuleFunc("oifname != \"lo\" counter jump " + l.chain)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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 Output interface {
|
||||
AddRule(expr ...string) error
|
||||
}
|
||||
|
||||
type output struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
chain string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
baseChain := nftChain.BaseChainOptions{
|
||||
Type: nftChain.TypeFilter,
|
||||
Hook: nftChain.HookOutput,
|
||||
Priority: int32(priority),
|
||||
Policy: policy,
|
||||
Device: "",
|
||||
}
|
||||
|
||||
if err := nft.Chain().Add(family, table, chain, baseChain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &output{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
chain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *output) AddRule(expr ...string) error {
|
||||
return c.nft.Rule().Add(c.family, c.table, c.chain, expr...)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
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"
|
||||
nftFamily "git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
)
|
||||
|
||||
type PacketFilter interface {
|
||||
AddRuleIn(AddRuleFunc func(expr ...string) error) error
|
||||
AddRuleOut(AddRuleFunc func(expr ...string) error) error
|
||||
}
|
||||
|
||||
type packetFilter struct {
|
||||
enable bool
|
||||
invalidName string
|
||||
}
|
||||
|
||||
// newPacketFilter Drop out of order packets and packets in an INVALID state in nftables connection tracking.
|
||||
func newPacketFilter(nft nft.NFT, family nftFamily.Type, table string, enable bool) (PacketFilter, error) {
|
||||
chainInvalidName := "INVALID"
|
||||
if !enable {
|
||||
return &packetFilter{
|
||||
enable: enable,
|
||||
invalidName: chainInvalidName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
chainName := "INVDROP"
|
||||
|
||||
if err := nft.Chain().Add(family, table, chainName, nftChain.TypeNone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := nft.Rule().Add(family, table, chainName, "counter drop"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Chain().Add(family, table, chainInvalidName, nftChain.TypeNone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Rule().Add(family, table, chainInvalidName, "ct state invalid counter jump INVDROP"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Rule().Add(family, table, chainInvalidName, "tcp flags ! fin,syn,rst,psh,ack,urg counter jump INVDROP"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Rule().Add(family, table, chainInvalidName, "tcp flags & (fin | syn | rst | psh | ack | urg) == fin | syn | rst | psh | ack | urg counter jump INVDROP"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Rule().Add(family, table, chainInvalidName, "tcp flags & (fin | syn) == fin | syn counter jump INVDROP"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Rule().Add(family, table, chainInvalidName, "tcp flags & (syn | rst) == syn | rst counter jump INVDROP"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Rule().Add(family, table, chainInvalidName, "tcp flags & (fin | rst) == fin | rst counter jump INVDROP"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Rule().Add(family, table, chainInvalidName, "tcp flags & (fin | ack) == fin counter jump INVDROP"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Rule().Add(family, table, chainInvalidName, "tcp flags & (psh | ack) == psh counter jump INVDROP"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Rule().Add(family, table, chainInvalidName, "tcp flags & (ack | urg) == urg counter jump INVDROP"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nft.Rule().Add(family, table, chainInvalidName, "tcp flags & (fin | syn | rst | ack) != syn ct state new counter jump INVDROP"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &packetFilter{
|
||||
enable: enable,
|
||||
invalidName: chainInvalidName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *packetFilter) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
|
||||
if !f.enable {
|
||||
return nil
|
||||
}
|
||||
return AddRuleFunc("iifname != \"lo\" meta l4proto tcp counter jump " + f.invalidName)
|
||||
}
|
||||
|
||||
func (f *packetFilter) AddRuleOut(AddRuleFunc func(expr ...string) error) error {
|
||||
if !f.enable {
|
||||
return nil
|
||||
}
|
||||
return AddRuleFunc("oifname != \"lo\" meta l4proto tcp counter jump " + f.invalidName)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package firewall
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -12,9 +13,11 @@ type Config struct {
|
||||
Options ConfigOptions
|
||||
MetadataNaming ConfigMetadata
|
||||
Policy ConfigPolicy
|
||||
PortKnocking []ConfigPortKnocking
|
||||
}
|
||||
|
||||
type ConfigOptions struct {
|
||||
Cache bool
|
||||
ClearMode ClearMode
|
||||
SavesRules bool
|
||||
SavesRulesPath string
|
||||
@@ -35,32 +38,14 @@ type ConfigPolicy struct {
|
||||
DefaultAllowInput bool
|
||||
DefaultAllowOutput bool
|
||||
DefaultAllowForward bool
|
||||
InputDrop PolicyDrop
|
||||
InputDrop types.PolicyDrop
|
||||
InputPriority int
|
||||
OutputDrop PolicyDrop
|
||||
OutputDrop types.PolicyDrop
|
||||
OutputPriority int
|
||||
ForwardDrop PolicyDrop
|
||||
ForwardDrop types.PolicyDrop
|
||||
ForwardPriority int
|
||||
}
|
||||
|
||||
type PolicyDrop int8
|
||||
|
||||
const (
|
||||
Drop PolicyDrop = iota + 1
|
||||
Reject
|
||||
)
|
||||
|
||||
func (p PolicyDrop) String() string {
|
||||
switch p {
|
||||
case Drop:
|
||||
return "drop"
|
||||
case Reject:
|
||||
return "reject"
|
||||
default:
|
||||
return "drop"
|
||||
}
|
||||
}
|
||||
|
||||
type ConfigIP4 struct {
|
||||
IcmpIn bool
|
||||
IcmpInRate string
|
||||
@@ -79,76 +64,30 @@ type ConfigIP6 struct {
|
||||
}
|
||||
|
||||
type ConfigPort struct {
|
||||
Number uint16
|
||||
Protocol Protocol
|
||||
Action Action
|
||||
Port types.L4Port
|
||||
Action types.Action
|
||||
LimitRate string
|
||||
}
|
||||
|
||||
type ConfigIP struct {
|
||||
IP string
|
||||
OnlyIP bool // Port is not taken into account
|
||||
Port uint16
|
||||
Action Action
|
||||
Protocol Protocol
|
||||
Port types.L4Port
|
||||
Action types.Action
|
||||
LimitRate string
|
||||
}
|
||||
|
||||
type Action int8
|
||||
|
||||
const (
|
||||
ActionAccept Action = iota + 1
|
||||
ActionReject
|
||||
ActionDrop
|
||||
)
|
||||
|
||||
func (a Action) String() string {
|
||||
switch a {
|
||||
case ActionAccept:
|
||||
return "accept"
|
||||
case ActionReject:
|
||||
return "reject"
|
||||
case ActionDrop:
|
||||
return "drop"
|
||||
default:
|
||||
return "drop"
|
||||
}
|
||||
type ConfigPortKnocking struct {
|
||||
Name string
|
||||
Port types.L4Port
|
||||
IPVersion ip.Version
|
||||
Knocks []*ConfigKnock
|
||||
}
|
||||
|
||||
type Protocol int8
|
||||
|
||||
const (
|
||||
ProtocolTCP Protocol = iota + 1
|
||||
ProtocolUDP
|
||||
)
|
||||
|
||||
func (p Protocol) String() string {
|
||||
switch p {
|
||||
case ProtocolTCP:
|
||||
return "tcp"
|
||||
case ProtocolUDP:
|
||||
return "udp"
|
||||
default:
|
||||
return fmt.Sprintf("Protocol(%d)", p)
|
||||
}
|
||||
}
|
||||
|
||||
type Direction int8
|
||||
|
||||
const (
|
||||
DirectionIn Direction = iota + 1
|
||||
DirectionOut
|
||||
)
|
||||
|
||||
func (d Direction) String() string {
|
||||
switch d {
|
||||
case DirectionIn:
|
||||
return "in"
|
||||
case DirectionOut:
|
||||
return "out"
|
||||
default:
|
||||
return fmt.Sprintf("Direction(%d)", d)
|
||||
}
|
||||
type ConfigKnock struct {
|
||||
Port types.L4Port
|
||||
Action types.KnockAction
|
||||
Timeout uint32
|
||||
}
|
||||
|
||||
type ClearMode int8
|
||||
@@ -1,72 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -2,18 +2,28 @@ package firewall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"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"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
nftables "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/blocklist"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
|
||||
dockerFirewall "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/firewall"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/blocking"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/config"
|
||||
nftFirewall "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft/table"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/reload"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/info"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/filesystem"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
// Reload Clear all rules and set new rules.
|
||||
Reload() error
|
||||
Reload(daemonInfo info.Info) error
|
||||
|
||||
// SavesRules Save rules to file.
|
||||
SavesRules()
|
||||
@@ -21,67 +31,119 @@ type API interface {
|
||||
// ClearRules Clear all rules.
|
||||
ClearRules()
|
||||
|
||||
// BlockIP Block IP address.
|
||||
BlockIP(blockIP blocking.BlockIP) (bool, error)
|
||||
|
||||
// BlockIPWithPorts Block IP address with ports.
|
||||
BlockIPWithPorts(blockIP blocking.BlockIPWithPorts) (bool, error)
|
||||
|
||||
// UnblockAllIPs Unblock all IP addresses.
|
||||
UnblockAllIPs() error
|
||||
|
||||
// UnblockIP Unblock IP address.
|
||||
UnblockIP(ip net.IP) error
|
||||
|
||||
// ClearDBData Clear all data from DB
|
||||
ClearDBData() error
|
||||
|
||||
// DockerSupport Return true if docker support
|
||||
DockerSupport() bool
|
||||
|
||||
// HasRules Return true if firewall has rules
|
||||
HasRules() (bool, error)
|
||||
}
|
||||
|
||||
type firewall struct {
|
||||
nft nftables.NFT
|
||||
logger log.Logger
|
||||
config *Config
|
||||
chains chain.Chains
|
||||
docker docker_monitor.Docker
|
||||
nft nftFirewall.NFT
|
||||
table table.Table
|
||||
logger log.Logger
|
||||
config *config.Config
|
||||
blockingService blocking.API
|
||||
docker docker_monitor.Docker
|
||||
blocklist blocklist.Blocklist
|
||||
dataDir string
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func New(pathNFT string, logger log.Logger, config Config, docker docker_monitor.Docker) (API, error) {
|
||||
nft, err := nftables.NewWithPath(pathNFT)
|
||||
func New(
|
||||
pathNFT string,
|
||||
blockingService blocking.API,
|
||||
logger log.Logger,
|
||||
config config.Config,
|
||||
docker docker_monitor.Docker,
|
||||
blocklist blocklist.Blocklist,
|
||||
dataDir string,
|
||||
) (API, error) {
|
||||
nftClient, err := nftables.NewWithPath(pathNFT)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create nft client: %w %s", err, pathNFT)
|
||||
}
|
||||
|
||||
return &firewall{
|
||||
nft: nft,
|
||||
logger: logger,
|
||||
config: &config,
|
||||
docker: docker,
|
||||
nft: nftFirewall.New(nftClient, strings.TrimRight(dataDir, "/")+"/tmp"),
|
||||
logger: logger,
|
||||
config: &config,
|
||||
blockingService: blockingService,
|
||||
docker: docker,
|
||||
blocklist: blocklist,
|
||||
dataDir: dataDir,
|
||||
|
||||
mu: sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *firewall) Reload() error {
|
||||
func (f *firewall) Reload(daemonInfo info.Info) error {
|
||||
f.logger.Debug("Reload nftables rules")
|
||||
if f.config.Options.ClearMode == ClearModeGlobal {
|
||||
if err := f.nft.Clear(); err != nil {
|
||||
|
||||
nftReload := reload.New(f.nft, f.logger, f.config)
|
||||
blocklistNames := f.blocklist.Names()
|
||||
|
||||
var nftTable table.Table
|
||||
var err error
|
||||
if f.config.Options.Cache {
|
||||
file := f.pathFileCacheNFT()
|
||||
nftTable, err = nftReload.RunWithCache(
|
||||
file,
|
||||
f.isValidCacheFile(daemonInfo),
|
||||
blocklistNames,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checksum, err := filesystem.FileChecksum(file)
|
||||
if err != nil {
|
||||
f.logger.Error(fmt.Sprintf("Failed to calculate checksum for %s: %s", file, err))
|
||||
} else if err := daemonInfo.Metadata().FirewallFileNft().Update(checksum); err != nil {
|
||||
f.logger.Error(fmt.Sprintf("Failed to update metadata: %s", err))
|
||||
}
|
||||
} else {
|
||||
nftTable, err = nftReload.Run(blocklistNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
chains, err := chain.NewChains(f.nft, f.config.MetadataNaming.TableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.chains = chains
|
||||
f.mu.Lock()
|
||||
f.table = nftTable
|
||||
f.mu.Unlock()
|
||||
|
||||
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
|
||||
}
|
||||
if err := f.reloadInput(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.reloadOutput(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.reloadForward(); err != nil {
|
||||
return err
|
||||
}
|
||||
if f.config.Options.DockerSupport {
|
||||
if err := f.reloadDocker(); err != nil {
|
||||
if f.config.Options.DockerSupport && nftTable.DockerChains() != nil {
|
||||
nftDocker := dockerFirewall.NewNFT(f.nft, nftTable.DockerChains())
|
||||
if err := f.docker.NftReload(nftDocker); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.blockingService.NftReload(f.nft, nftTable.BlockList().ListIP(), nftTable.BlockList().ListIPWithPort()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.blocklist.NftReload(nftTable.BlockList().Blocks()); err != nil {
|
||||
f.logger.Error(fmt.Sprintf("Failed to reload blocklist: %s", err))
|
||||
}
|
||||
|
||||
f.logger.Debug("Reload nftables rules done")
|
||||
return nil
|
||||
}
|
||||
@@ -90,13 +152,17 @@ func (f *firewall) ClearRules() {
|
||||
f.logger.Debug("Clear nftables rules")
|
||||
|
||||
switch f.config.Options.ClearMode {
|
||||
case ClearModeGlobal:
|
||||
if err := f.nft.Clear(); err != nil {
|
||||
case config.ClearModeGlobal:
|
||||
if err := f.nft.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 {
|
||||
case config.ClearModeOwn:
|
||||
if f.table == nil {
|
||||
f.logger.Error("table is nil")
|
||||
return
|
||||
}
|
||||
if err := f.table.Clear(); err != nil {
|
||||
f.logger.Error(fmt.Sprintf("Failed to clear rules: %s", err))
|
||||
}
|
||||
break
|
||||
@@ -105,6 +171,18 @@ func (f *firewall) ClearRules() {
|
||||
f.logger.Debug("Clear nftables rules done")
|
||||
}
|
||||
|
||||
func (f *firewall) UnblockAllIPs() error {
|
||||
return f.blockingService.UnblockAllIPs()
|
||||
}
|
||||
|
||||
func (f *firewall) UnblockIP(ip net.IP) error {
|
||||
return f.blockingService.UnblockIP(ip)
|
||||
}
|
||||
|
||||
func (f *firewall) ClearDBData() error {
|
||||
return f.blockingService.ClearDBData()
|
||||
}
|
||||
|
||||
func (f *firewall) SavesRules() {
|
||||
if !f.config.Options.SavesRules {
|
||||
f.logger.Debug("SavesRules is false, skip")
|
||||
@@ -117,7 +195,7 @@ func (f *firewall) SavesRules() {
|
||||
}
|
||||
|
||||
args := []string{"list", "ruleset"}
|
||||
output, err := f.nft.Command().RunWithOutput(args...)
|
||||
output, err := f.nft.NFT().Command().RunWithOutput(args...)
|
||||
if err != nil {
|
||||
f.logger.Warn(fmt.Sprintf("Failed to save rules: %s", err))
|
||||
return
|
||||
@@ -133,6 +211,72 @@ func (f *firewall) SavesRules() {
|
||||
f.logger.Info("Save nftables rules")
|
||||
}
|
||||
|
||||
func (f *firewall) HasRules() (bool, error) {
|
||||
return f.table.HasRules()
|
||||
}
|
||||
|
||||
func (f *firewall) BlockIP(blockIP blocking.BlockIP) (bool, error) {
|
||||
isBanned, err := f.blockingService.BlockIP(blockIP)
|
||||
|
||||
if err != nil {
|
||||
f.logger.Warn(fmt.Sprintf("Failed to block ip %s: %s", blockIP.IP.String(), err))
|
||||
}
|
||||
return isBanned, err
|
||||
}
|
||||
|
||||
func (f *firewall) BlockIPWithPorts(blockIP blocking.BlockIPWithPorts) (bool, error) {
|
||||
isBanned, err := f.blockingService.BlockIPWithPorts(blockIP)
|
||||
|
||||
if err != nil {
|
||||
f.logger.Warn(fmt.Sprintf("Failed to block ip %s: %s", blockIP.IP.String(), err))
|
||||
}
|
||||
return isBanned, err
|
||||
}
|
||||
|
||||
func (f *firewall) DockerSupport() bool {
|
||||
return f.config.Options.DockerSupport
|
||||
}
|
||||
|
||||
func (f *firewall) pathFileCacheNFT() string {
|
||||
return strings.TrimRight(f.dataDir, "/") + "/nftables.nft"
|
||||
}
|
||||
|
||||
func (f *firewall) isValidCacheFile(daemonInfo info.Info) bool {
|
||||
if daemonInfo.IsVersionChanged() {
|
||||
f.logger.Debug("Version changed, skip cache")
|
||||
return false
|
||||
}
|
||||
|
||||
if daemonInfo.IsSettingsChanged() {
|
||||
f.logger.Debug("Settings changed, skip cache")
|
||||
return false
|
||||
}
|
||||
|
||||
fileNFT := f.pathFileCacheNFT()
|
||||
if !filesystem.FileExists(fileNFT) {
|
||||
return false
|
||||
}
|
||||
|
||||
metadataChecksum, err := daemonInfo.Metadata().FirewallFileNft().Get()
|
||||
if err != nil {
|
||||
f.logger.Error(fmt.Sprintf("Failed to get checksum: %s", err))
|
||||
return false
|
||||
}
|
||||
|
||||
if metadataChecksum == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
checksum, err := filesystem.FileChecksum(fileNFT)
|
||||
if err != nil {
|
||||
f.logger.Error(fmt.Sprintf("Failed to calculate checksum for %s: %s", fileNFT, err))
|
||||
return false
|
||||
}
|
||||
|
||||
if checksum != metadataChecksum {
|
||||
f.logger.Warn(fmt.Sprintf("Checksum of %s is not equal to metadata checksum", fileNFT))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
type GuardConfig struct {
|
||||
Enable bool
|
||||
Notifications bool
|
||||
Recovery bool
|
||||
Interval uint32
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package guard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/guard/config"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/info"
|
||||
"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 firewallGuardTarget interface {
|
||||
HasRules() (bool, error)
|
||||
Reload(daemonInfo info.Info) error
|
||||
}
|
||||
|
||||
type RulesGuard interface {
|
||||
Run(daemonInfo info.Info, ctx context.Context)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type rulesGuard struct {
|
||||
config *config.GuardConfig
|
||||
firewall firewallGuardTarget
|
||||
notify notifications.Notifications
|
||||
logger log.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewRulesGuard(config *config.GuardConfig, firewall firewallGuardTarget, notify notifications.Notifications, logger log.Logger) RulesGuard {
|
||||
return &rulesGuard{
|
||||
config: config,
|
||||
firewall: firewall,
|
||||
notify: notify,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *rulesGuard) Run(daemonInfo info.Info, ctx context.Context) {
|
||||
if !g.config.Enable {
|
||||
g.logger.Debug("firewall rules guard is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
g.logger.Debug("firewall rules guard is enabled")
|
||||
guardCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
g.mu.Lock()
|
||||
g.cancel = cancel
|
||||
g.mu.Unlock()
|
||||
|
||||
go g.run(daemonInfo, guardCtx)
|
||||
}
|
||||
|
||||
func (g *rulesGuard) Close() error {
|
||||
g.mu.Lock()
|
||||
cancel := g.cancel
|
||||
g.cancel = nil
|
||||
g.mu.Unlock()
|
||||
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *rulesGuard) run(daemonInfo info.Info, ctx context.Context) {
|
||||
interval := time.Duration(g.config.Interval) * time.Second
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
g.checkRules(daemonInfo)
|
||||
|
||||
case <-ctx.Done():
|
||||
g.logger.Debug("firewall rules guard stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *rulesGuard) checkRules(daemonInfo info.Info) {
|
||||
hasRules, err := g.firewall.HasRules()
|
||||
if err != nil {
|
||||
g.logger.Error(fmt.Sprintf("failed to check firewall rules: %s", err))
|
||||
if g.config.Notifications {
|
||||
message := notifications.Message{
|
||||
Subject: i18n.Lang.T("firewall_rules_not_found"),
|
||||
Body: i18n.Lang.T("firewall_rules_not_found_body_check_error", map[string]interface{}{
|
||||
"Error": err.Error(),
|
||||
}),
|
||||
}
|
||||
g.notify.SendAsync(message)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if hasRules {
|
||||
g.logger.Debug("firewall rules exists")
|
||||
return
|
||||
}
|
||||
|
||||
g.logger.Warn("firewall rules not found")
|
||||
if g.config.Recovery {
|
||||
if err := g.firewall.Reload(daemonInfo); err != nil {
|
||||
g.logger.Error(fmt.Sprintf("failed to recover firewall rules: %s", err))
|
||||
if g.config.Notifications {
|
||||
message := notifications.Message{
|
||||
Subject: i18n.Lang.T("firewall_rules_not_found"),
|
||||
Body: i18n.Lang.T("firewall_rules_not_found_body_recover_error", map[string]interface{}{
|
||||
"Error": err.Error(),
|
||||
}),
|
||||
}
|
||||
g.notify.SendAsync(message)
|
||||
}
|
||||
return
|
||||
}
|
||||
g.logger.Warn("firewall rules recovered")
|
||||
if g.config.Notifications {
|
||||
message := notifications.Message{
|
||||
Subject: i18n.Lang.T("firewall_rules_not_found"),
|
||||
Body: i18n.Lang.T("firewall_rules_not_found_body_recover_success"),
|
||||
}
|
||||
g.notify.SendAsync(message)
|
||||
}
|
||||
} else if g.config.Notifications {
|
||||
message := notifications.Message{
|
||||
Subject: i18n.Lang.T("firewall_rules_not_found"),
|
||||
Body: i18n.Lang.T("firewall_rules_not_found_body"),
|
||||
}
|
||||
g.notify.SendAsync(message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package block
|
||||
|
||||
import (
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client/contract"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
)
|
||||
|
||||
type Sets interface {
|
||||
Add(name string, params string) error
|
||||
}
|
||||
|
||||
type setBatch struct {
|
||||
builder nft.BatchBuilder
|
||||
family family.Type
|
||||
table string
|
||||
}
|
||||
|
||||
func NewBatchSet(builder nft.BatchBuilder, family family.Type, table string) Sets {
|
||||
return &setBatch{
|
||||
builder: builder,
|
||||
family: family,
|
||||
table: table,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *setBatch) Add(name string, params string) error {
|
||||
command := []string{
|
||||
"add set", b.family.String(), b.table, name, "{ " + params + " }",
|
||||
}
|
||||
return b.builder.Command().Run(command...)
|
||||
}
|
||||
|
||||
func getNamesIP(name string) (ipV4 string, ipV6 string) {
|
||||
return name + "_ip4", name + "_ip6"
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package block
|
||||
|
||||
import (
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client/contract"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
nftFirewall "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft/rule"
|
||||
)
|
||||
|
||||
type Blocklist interface {
|
||||
// ReplaceElements Replace the elements of the list.
|
||||
ReplaceElements(ipV4 []string, ipV6 []string, pathSaveNft string) error
|
||||
|
||||
ReplaceElementsWithFile(pathNft string) error
|
||||
|
||||
// AddRuleToChain Add a rule to the parent chain.
|
||||
AddRuleToChain(chainAddRuleFunc rule.AddFunc, action string) error
|
||||
}
|
||||
|
||||
type blocklist struct {
|
||||
nft nftFirewall.NFT
|
||||
|
||||
listIPv4 List
|
||||
listIPv6 List
|
||||
}
|
||||
|
||||
func NewBlocklist(nft nftFirewall.NFT, builder nft.BatchBuilder, family family.Type, table string, name string) (Blocklist, error) {
|
||||
listNameV4, listNameV6 := getNamesIP(name)
|
||||
|
||||
params := "type ipv4_addr; flags interval; auto-merge;"
|
||||
listIPv4, err := newList(nft, builder, family, table, listNameV4, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params = "type ipv6_addr; flags interval; auto-merge;"
|
||||
listIPv6, err := newList(nft, builder, family, table, listNameV6, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &blocklist{
|
||||
nft: nft,
|
||||
|
||||
listIPv4: listIPv4,
|
||||
listIPv6: listIPv6,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewBlocklistWithoutCommand(nft nftFirewall.NFT, family family.Type, table string, name string) Blocklist {
|
||||
listNameV4, listNameV6 := getNamesIP(name)
|
||||
|
||||
listIPv4 := newListWithoutCommand(nft, family, table, listNameV4)
|
||||
listIPv6 := newListWithoutCommand(nft, family, table, listNameV6)
|
||||
|
||||
return &blocklist{
|
||||
nft: nft,
|
||||
|
||||
listIPv4: listIPv4,
|
||||
listIPv6: listIPv6,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *blocklist) ReplaceElements(ipV4 []string, ipV6 []string, pathSaveNft string) error {
|
||||
batchBuilder, err := l.nft.NewBuildBatch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = batchBuilder.Close()
|
||||
}()
|
||||
|
||||
if err := l.listIPv4.ReplaceBatchElements(batchBuilder, ipV4); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.listIPv6.ReplaceBatchElements(batchBuilder, ipV6); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.nft.RunBatchAndMoveFile(batchBuilder, pathSaveNft)
|
||||
}
|
||||
|
||||
func (l *blocklist) ReplaceElementsWithFile(pathNft string) error {
|
||||
args := []string{"-f", pathNft}
|
||||
return l.nft.NFT().Command().Run(args...)
|
||||
}
|
||||
|
||||
func (l *blocklist) AddRuleToChain(chainAddRuleFunc rule.AddFunc, action string) error {
|
||||
addRule := "ip saddr @" + l.listIPv4.Name() + " " + action
|
||||
if err := chainAddRuleFunc(addRule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addRule = "ip6 saddr @" + l.listIPv6.Name() + " " + action
|
||||
if err := chainAddRuleFunc(addRule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package block
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client/contract"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
nftFirewall "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft"
|
||||
)
|
||||
|
||||
type List interface {
|
||||
Name() string
|
||||
AddElement(element string) error
|
||||
AddBatchElement(builder nft.BatchBuilder, element string) error
|
||||
DeleteElement(element string) error
|
||||
ReplaceElements(elements []string) error
|
||||
ReplaceBatchElements(builder nft.BatchBuilder, elements []string) error
|
||||
}
|
||||
|
||||
type list struct {
|
||||
nft nftFirewall.NFT
|
||||
family family.Type
|
||||
table string
|
||||
name string
|
||||
}
|
||||
|
||||
func newList(nft nftFirewall.NFT, builder nft.BatchBuilder, family family.Type, table string, name string, params string) (List, error) {
|
||||
command := []string{
|
||||
"add set", family.String(), table, name, "{ " + params + " }",
|
||||
}
|
||||
if err := builder.Command().Run(command...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &list{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newListWithoutCommand(nft nftFirewall.NFT, family family.Type, table string, name string) List {
|
||||
return &list{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *list) Name() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
func (l *list) AddElement(element string) error {
|
||||
command := []string{
|
||||
"add element",
|
||||
l.family.String(), l.table, l.name,
|
||||
fmt.Sprintf("{ %s }", element),
|
||||
}
|
||||
return l.nft.NFT().Command().Run(command...)
|
||||
}
|
||||
|
||||
func (l *list) AddBatchElement(builder nft.BatchBuilder, element string) error {
|
||||
command := []string{
|
||||
"add element",
|
||||
l.family.String(), l.table, l.name,
|
||||
fmt.Sprintf("{ %s }", element),
|
||||
}
|
||||
return builder.Command().Run(command...)
|
||||
}
|
||||
|
||||
func (l *list) DeleteElement(element string) error {
|
||||
command := []string{
|
||||
"delete element",
|
||||
l.family.String(), l.table, l.name,
|
||||
fmt.Sprintf("{ %s }", element),
|
||||
}
|
||||
return l.nft.NFT().Command().Run(command...)
|
||||
}
|
||||
|
||||
func (l *list) ReplaceElements(elements []string) error {
|
||||
batchBuilder, err := l.nft.NewBuildBatch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = batchBuilder.Close()
|
||||
}()
|
||||
|
||||
if err := l.replaceElements(batchBuilder, elements); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.nft.RunBatch(batchBuilder)
|
||||
}
|
||||
|
||||
func (l *list) ReplaceBatchElements(builder nft.BatchBuilder, elements []string) error {
|
||||
return l.replaceElements(builder, elements)
|
||||
}
|
||||
|
||||
func (l *list) replaceElements(builder nft.BatchBuilder, elements []string) error {
|
||||
if err := builder.Command().Run("flush set", l.family.String(), l.table, l.name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(elements) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := []string{
|
||||
"add element",
|
||||
l.family.String(), l.table, l.name,
|
||||
fmt.Sprintf("{ %s }", strings.Join(elements, ",")),
|
||||
}
|
||||
|
||||
return builder.Command().Run(command...)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user