Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
+59
-3
@@ -1,15 +1,71 @@
|
||||
## 0.7.0 (8.2.2026)
|
||||
## 0.9.0 (21.3.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 (9.3.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.2.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Добавлена возможность настройки отслеживания событий в журналах.
|
||||
* Добавлены настройки для защиты от перебора паролей.
|
||||
* В файл настроек analyzer.toml добавлены новые параметры. Смотрите полный список по ссылке https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/commit/187c447301b9c0bfa41ec2b2c9435ab0ce44bed6/assets/configs/analyzer.toml
|
||||
* В файл настроек 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
|
||||
* 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 (8.2.2026)
|
||||
|
||||
@@ -87,7 +87,7 @@ ssh_enable = true
|
||||
ssh_notify = true
|
||||
|
||||
###
|
||||
# Можно указать свою группу, чтобы по связать с другими правилами.
|
||||
# Можно указать свою группу, чтобы связать с другими правилами.
|
||||
# По умолчанию: ""
|
||||
# ***
|
||||
# You can specify your own group to link it to other rules.
|
||||
@@ -102,12 +102,17 @@ ssh_group = ""
|
||||
# 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 обнаружений, то сработает блокировка.
|
||||
## И теперь заблокирует на час.
|
||||
@@ -127,12 +132,17 @@ ssh_group = ""
|
||||
# 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.
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Настройки для определения списков блокировки 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
|
||||
|
||||
###
|
||||
# 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
|
||||
###
|
||||
@@ -508,6 +508,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.
|
||||
###
|
||||
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Именование метаданных
|
||||
# ***
|
||||
|
||||
@@ -273,3 +273,14 @@ 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"
|
||||
|
||||
@@ -3,6 +3,7 @@ module git.kor-elf.net/kor-elf-shield/kor-elf-shield
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
git.kor-elf.net/kor-elf-shield/blocklist v1.1.0
|
||||
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/nxadm/tail v1.4.11
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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/go-nftables-client v0.1.1 h1:3oGtZ/r1YAdlvI16OkZSCaxcWztHe/33ITWfI2LaQm0=
|
||||
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1/go.mod h1:a7F+XdL1pK5P3ucQRR2EK/fABAP37LLBENiA4hX7L6A=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/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=
|
||||
@@ -10,8 +11,6 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
|
||||
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/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=
|
||||
@@ -20,8 +19,6 @@ 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=
|
||||
@@ -46,8 +43,6 @@ 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/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||
@@ -56,26 +51,17 @@ go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
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/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.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
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/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
func CmdBan() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ban",
|
||||
Usage: i18n.Lang.T("cmd.daemon.ban.Usage"),
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "clear",
|
||||
Usage: i18n.Lang.T("cmd.daemon.ban.clear.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.ban.clear.Description"),
|
||||
Action: CmdBanClear,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CmdBanClear(_ 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)
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.Send("ban_clear")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result != "ok" {
|
||||
return errors.New(i18n.Lang.T("ban_clear_error"))
|
||||
}
|
||||
|
||||
fmt.Println(i18n.Lang.T("ban_clear_success"))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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,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)
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -39,10 +37,7 @@ func CmdNotifications() *cli.Command {
|
||||
}
|
||||
|
||||
func cmdNotificationsQueueCount(_ 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"))
|
||||
}
|
||||
@@ -63,10 +58,7 @@ func cmdNotificationsQueueCount(_ context.Context, _ *cli.Command) error {
|
||||
}
|
||||
|
||||
func cmdNotificationsQueueClear(_ 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"))
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/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"
|
||||
@@ -75,7 +76,9 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
d, err := daemon.NewDaemon(config, logger, notificationsService, dockerService)
|
||||
blocklistService := newBlocklistService(ctx, repositories.Blocklist(), logger)
|
||||
|
||||
d, err := daemon.NewDaemon(config, logger, notificationsService, dockerService, blocklistService)
|
||||
if err != nil {
|
||||
logger.Fatal(err.Error())
|
||||
|
||||
@@ -123,3 +126,27 @@ 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,
|
||||
}
|
||||
|
||||
blocklistService, err := blocklist.New(blocklistConfig, ctx, logger)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
return blocklist.NewFalseBlocklist()
|
||||
}
|
||||
|
||||
return blocklistService
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func NewMainApp(appVer AppVersion, defaultConfigPath string) *cli.Command {
|
||||
daemon.CmdStatus(),
|
||||
daemon.CmdReopenLogger(),
|
||||
daemon.CmdNotifications(),
|
||||
daemon.CmdBan(),
|
||||
daemon.CmdBlock(),
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
@@ -7,6 +7,7 @@ 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/notifications"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
@@ -29,7 +30,7 @@ type analyzer struct {
|
||||
logChan chan analysisServices.Entry
|
||||
}
|
||||
|
||||
func New(config config2.Config, blockIPFunc analysisServices.BlockIPFunc, repositories db.Repositories, logger log.Logger, notify notifications.Notifications) Analyzer {
|
||||
func New(config config2.Config, blockService brute_force_protection_group.BlockService, repositories db.Repositories, logger log.Logger, notify notifications.Notifications) Analyzer {
|
||||
var journalMatches []string
|
||||
journalMatchesUniq := map[string]struct{}{}
|
||||
|
||||
@@ -65,7 +66,7 @@ func New(config config2.Config, blockIPFunc analysisServices.BlockIPFunc, reposi
|
||||
|
||||
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, journalMatches, logger)
|
||||
filesService := analyzerLog.NewFileMonitoring(files, logger)
|
||||
analysisService := analyzerLog.NewAnalysis(rulesIndex, blockIPFunc, repositories, logger, notify)
|
||||
analysisService := analyzerLog.NewAnalysis(rulesIndex, blockService, repositories, logger, notify)
|
||||
|
||||
return &analyzer{
|
||||
config: config,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -20,6 +20,7 @@ type RateLimit struct {
|
||||
Count uint32
|
||||
Period uint32
|
||||
BlockingTimeSeconds uint32
|
||||
BlockConfig Block
|
||||
}
|
||||
|
||||
type PatternValue struct {
|
||||
|
||||
@@ -22,13 +22,13 @@ type analysis struct {
|
||||
bruteForceProtectionService analysisServices.BruteForceProtection
|
||||
}
|
||||
|
||||
func NewAnalysis(rulesIndex *analysisServices.RulesIndex, blockIPFunc analysisServices.BlockIPFunc, repositories db.Repositories, 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) Analysis {
|
||||
alertGroupService := alert_group.NewGroup(repositories.AlertGroup(), logger)
|
||||
bruteForceProtectionGroupService := brute_force_protection_group.NewGroup(repositories.BruteForceProtectionGroup(), logger)
|
||||
|
||||
return &analysis{
|
||||
alertService: analysisServices.NewAlert(rulesIndex, alertGroupService, logger, notify),
|
||||
bruteForceProtectionService: analysisServices.NewBruteForceProtection(rulesIndex, bruteForceProtectionGroupService, blockIPFunc, logger, notify),
|
||||
bruteForceProtectionService: analysisServices.NewBruteForceProtection(rulesIndex, bruteForceProtectionGroupService, blockService, logger, notify),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ package analysis
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"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/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/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"
|
||||
@@ -18,12 +20,10 @@ type BruteForceProtection interface {
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type BlockIPFunc func(blockIP blocking.BlockIP) (bool, error)
|
||||
|
||||
type bruteForceProtection struct {
|
||||
rulesIndex *RulesIndex
|
||||
groupService brute_force_protection_group.Group
|
||||
blockIP BlockIPFunc
|
||||
blockService brute_force_protection_group.BlockService
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
}
|
||||
@@ -38,17 +38,18 @@ type bruteForceProtectionNotify struct {
|
||||
rule *brute_force_protection.Rule
|
||||
messages []string
|
||||
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, blockIP BlockIPFunc, logger log.Logger, notify notifications.Notifications) BruteForceProtection {
|
||||
func NewBruteForceProtection(rulesIndex *RulesIndex, groupService brute_force_protection_group.Group, blockService brute_force_protection_group.BlockService, logger log.Logger, notify notifications.Notifications) BruteForceProtection {
|
||||
return &bruteForceProtection{
|
||||
rulesIndex: rulesIndex,
|
||||
groupService: groupService,
|
||||
blockIP: blockIP,
|
||||
blockService: blockService,
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
}
|
||||
@@ -81,29 +82,35 @@ func (p *bruteForceProtection) Analyze(entry *Entry) {
|
||||
continue
|
||||
}
|
||||
|
||||
blockIP := blocking.BlockIP{
|
||||
IP: result.ip,
|
||||
TimeSeconds: groupResult.BlockSec,
|
||||
Reason: rule.Message,
|
||||
}
|
||||
|
||||
isBanned, err := p.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,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
ipWithPorts, l4Ports := groupResult.BlockConfig.PortsBlocked()
|
||||
if !ipWithPorts {
|
||||
p.handleBlockIP(entry, rule, &result, &groupResult)
|
||||
continue
|
||||
}
|
||||
|
||||
p.logger.Info(fmt.Sprintf("Block IP %s detected (%s) (group:%s): %s", result.ip, rule.Name, rule.Group.Name, entry.Message))
|
||||
p.sendNotify(&bruteForceProtectionNotify{
|
||||
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,
|
||||
@@ -112,11 +119,61 @@ func (p *bruteForceProtection) Analyze(entry *Entry) {
|
||||
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,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) ClearDBData() error {
|
||||
return p.groupService.ClearDBData()
|
||||
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,
|
||||
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,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) analyzeRule(rule *brute_force_protection.Rule, message string) bruteForceProtectionAnalyzeRuleReturn {
|
||||
@@ -171,19 +228,45 @@ func (p *bruteForceProtection) analyzeRule(rule *brute_force_protection.Rule, me
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) sendNotify(notify *bruteForceProtectionNotify) {
|
||||
func (p *bruteForceProtection) sendNotifySuccess(notify *bruteForceProtectionNotify) {
|
||||
if !notify.rule.IsNotification {
|
||||
return
|
||||
}
|
||||
|
||||
groupName := notify.rule.Group.Name
|
||||
groupMessage := notify.rule.Group.Message + "\n\n"
|
||||
|
||||
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{
|
||||
@@ -191,6 +274,15 @@ func (p *bruteForceProtection) sendNotify(notify *bruteForceProtectionNotify) {
|
||||
}) + "\n"
|
||||
}
|
||||
text += "IP: " + notify.ip.String() + "\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": notify.blockSec,
|
||||
}) + "\n"
|
||||
@@ -206,36 +298,3 @@ func (p *bruteForceProtection) sendNotify(notify *bruteForceProtectionNotify) {
|
||||
}
|
||||
p.notify.SendAsync(notifications.Message{Subject: subject, Body: text})
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) sendNotifyError(notify *bruteForceProtectionNotify) {
|
||||
if !notify.rule.IsNotification {
|
||||
return
|
||||
}
|
||||
|
||||
groupName := notify.rule.Group.Name
|
||||
groupMessage := notify.rule.Group.Message + "\n\n"
|
||||
|
||||
subject := i18n.Lang.T("alert.bruteForceProtection.subject-error", map[string]any{
|
||||
"Name": notify.rule.Name,
|
||||
"GroupName": groupName,
|
||||
"IP": notify.ip,
|
||||
})
|
||||
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"
|
||||
}
|
||||
text += "IP: " + notify.ip.String() + "\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 += "\n" + i18n.Lang.T("log") + "\n"
|
||||
for _, message := range notify.messages {
|
||||
text += message + "\n"
|
||||
}
|
||||
p.notify.SendAsync(notifications.Message{Subject: subject, Body: text})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -23,9 +23,10 @@ type group struct {
|
||||
}
|
||||
|
||||
type AnalysisResult struct {
|
||||
Block bool
|
||||
BlockSec uint32
|
||||
LastLogs []string
|
||||
Block bool
|
||||
BlockSec uint32
|
||||
BlockConfig brute_force_protection.Block
|
||||
LastLogs []string
|
||||
}
|
||||
|
||||
func NewGroup(groupRepository repository.BruteForceProtectionGroupRepository, logger log.Logger) Group {
|
||||
@@ -99,6 +100,7 @@ func (g *group) analysisResult(rateLimit brute_force_protection.RateLimit, event
|
||||
analysisResult.LastLogs = entityGroup.LastLogs
|
||||
analysisResult.Block = true
|
||||
analysisResult.BlockSec = rateLimit.BlockingTimeSeconds
|
||||
analysisResult.BlockConfig = rateLimit.BlockConfig
|
||||
|
||||
entityGroup.CurrentLevelTriggerCount++
|
||||
entityGroup.TriggerCount = 0
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package blocklist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/chain/block"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type newBlocklist func(name string) (block.Blocklist, error)
|
||||
|
||||
type Blocklist interface {
|
||||
NftReload(newBlocklist newBlocklist) error
|
||||
Run()
|
||||
Close() error
|
||||
}
|
||||
|
||||
type updateSource struct {
|
||||
forcedly bool
|
||||
source *SourceConfig
|
||||
}
|
||||
|
||||
type blocklist struct {
|
||||
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) {
|
||||
return &blocklist{
|
||||
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) NftReload(newBlocklist newBlocklist) error {
|
||||
b.logger.Debug("Reload blocklist")
|
||||
for _, source := range b.Sources {
|
||||
b.logger.Debug(fmt.Sprintf("Reload blocklist from %s", source.Name))
|
||||
if source.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
nftBlocklist, err := newBlocklist("blocklist_" + source.Name)
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to create blocklist: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
b.nftBlocklists[source.Name] = nftBlocklist
|
||||
b.mu.Unlock()
|
||||
|
||||
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 listEntity.IsFresh(source.Interval) {
|
||||
if err := nftBlocklist.ReplaceElementsIPv4(listEntity.IPsV4); len(listEntity.IPsV4) > 0 && err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to replace elements (IPv4): %s", err))
|
||||
}
|
||||
|
||||
if err := nftBlocklist.ReplaceElementsIPv6(listEntity.IPsV6); len(listEntity.IPsV6) > 0 && err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to replace elements (IPv6): %s", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 listEntity.IsFresh(updSource.source.Interval) {
|
||||
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(),
|
||||
IPsV4: nil,
|
||||
IPsV6: nil,
|
||||
}
|
||||
|
||||
if len(ipsV4) > 0 {
|
||||
if err := nftBlocklist.ReplaceElementsIPv4(ipsV4); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to replace elements (IPv4): %s", err))
|
||||
} else {
|
||||
listEntity.IPsV4 = ipsV4
|
||||
}
|
||||
}
|
||||
|
||||
if len(ipsV6) > 0 {
|
||||
if err := nftBlocklist.ReplaceElementsIPv6(ipsV6); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to replace elements (IPv6): %s", err))
|
||||
} else {
|
||||
listEntity.IPsV6 = ipsV6
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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
|
||||
}
|
||||
|
||||
type SourceConfig struct {
|
||||
Name string
|
||||
Interval time.Duration
|
||||
Source sources.BlocklistSource
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package blocklist
|
||||
|
||||
type FalseBlocklist struct {
|
||||
}
|
||||
|
||||
func NewFalseBlocklist() Blocklist {
|
||||
return &FalseBlocklist{}
|
||||
}
|
||||
|
||||
func (b *FalseBlocklist) NftReload(_ newBlocklist) 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)
|
||||
}
|
||||
+142
-3
@@ -4,16 +4,23 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"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/types"
|
||||
"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/ip"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
)
|
||||
|
||||
type Daemon interface {
|
||||
@@ -29,6 +36,7 @@ type daemon struct {
|
||||
notifications notifications.Notifications
|
||||
analyzer analyzer.Analyzer
|
||||
docker docker_monitor.Docker
|
||||
blocklist blocklist.Blocklist
|
||||
|
||||
stopCh chan struct{}
|
||||
}
|
||||
@@ -77,6 +85,11 @@ func (d *daemon) Run(ctx context.Context, isTesting bool, testingInterval uint16
|
||||
}()
|
||||
}
|
||||
|
||||
d.blocklist.Run()
|
||||
defer func() {
|
||||
_ = d.blocklist.Close()
|
||||
}()
|
||||
|
||||
go d.socket.Run(ctx, d.socketCommand)
|
||||
d.runWorker(ctx, isTesting, testingInterval)
|
||||
|
||||
@@ -128,7 +141,7 @@ 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{}{}
|
||||
@@ -150,9 +163,50 @@ func (d *daemon) socketCommand(command string, socket socket.Connect) error {
|
||||
return err
|
||||
}
|
||||
return socket.Write("ok")
|
||||
case "ban_clear":
|
||||
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("ban clear failed: " + err.Error())
|
||||
_ = socket.Write("block clear failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
return socket.Write("ok")
|
||||
@@ -161,3 +215,88 @@ func (d *daemon) socketCommand(command string, socket socket.Connect) error {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type Repositories interface {
|
||||
AlertGroup() repository.AlertGroupRepository
|
||||
BruteForceProtectionGroup() repository.BruteForceProtectionGroupRepository
|
||||
Blocking() repository.BlockingRepository
|
||||
Blocklist() repository.BlocklistRepository
|
||||
|
||||
Close() error
|
||||
}
|
||||
@@ -28,6 +29,7 @@ type repositories struct {
|
||||
alertGroup repository.AlertGroupRepository
|
||||
bruteForceProtectionGroup repository.BruteForceProtectionGroupRepository
|
||||
blocking repository.BlockingRepository
|
||||
blocklist repository.BlocklistRepository
|
||||
|
||||
db []*bbolt.DB
|
||||
}
|
||||
@@ -57,6 +59,7 @@ func New(dataDir string) (Repositories, error) {
|
||||
alertGroup: repository.NewAlertGroupRepository(appDB),
|
||||
bruteForceProtectionGroup: repository.NewBruteForceProtectionGroupRepository(securityDB),
|
||||
blocking: repository.NewBlockingRepository(securityDB),
|
||||
blocklist: repository.NewBlocklistRepository(securityDB),
|
||||
|
||||
db: []*bbolt.DB{appDB, securityDB},
|
||||
}, nil
|
||||
@@ -78,6 +81,10 @@ func (r *repositories) Blocking() repository.BlockingRepository {
|
||||
return r.blocking
|
||||
}
|
||||
|
||||
func (r *repositories) Blocklist() repository.BlocklistRepository {
|
||||
return r.blocklist
|
||||
}
|
||||
|
||||
func (r *repositories) Close() error {
|
||||
for _, db := range r.db {
|
||||
_ = db.Close()
|
||||
|
||||
@@ -1,7 +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,17 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Blocklist struct {
|
||||
UpdatedAtUnix int64 `json:"UpdateAtUnix"`
|
||||
IPsV4 []string `json:"IPsV4"`
|
||||
IPsV6 []string `json:"IPsV6"`
|
||||
}
|
||||
|
||||
// IsFresh returns true if the blocklist is fresh.
|
||||
func (b *Blocklist) IsFresh(interval time.Duration) bool {
|
||||
lastUpdate := time.Unix(b.UpdatedAtUnix, 0)
|
||||
return b.UpdatedAtUnix > 0 && time.Since(lastUpdate) <= interval
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package repository
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
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
|
||||
}
|
||||
@@ -73,6 +75,44 @@ func (r *blocking) List(callback func(entity.Blocking) error) error {
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const (
|
||||
alertGroupBucket = "alert_group"
|
||||
bruteForceProtectionGroupBucket = "brute_force_protection_group"
|
||||
blockingBucket = "blocking"
|
||||
blocklistBucket = "blocklist"
|
||||
)
|
||||
|
||||
func nextID(b *bbolt.Bucket) ([]byte, error) {
|
||||
|
||||
@@ -10,20 +10,24 @@ import (
|
||||
"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/chain/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(blockListIP block.ListIP) error
|
||||
BlockIP(blockIP BlockIP) (bool, error)
|
||||
NftReload(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
|
||||
logger log.Logger
|
||||
blockingRepository repository.BlockingRepository
|
||||
blockListIP block.ListIP
|
||||
blockListIPWithPort block.ListIPWithPort
|
||||
logger log.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
@@ -34,6 +38,13 @@ type BlockIP struct {
|
||||
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,
|
||||
@@ -42,9 +53,10 @@ func New(blockingRepository repository.BlockingRepository, logger log.Logger) AP
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blocking) NftReload(blockListIP block.ListIP) error {
|
||||
func (b *blocking) NftReload(blockListIP block.ListIP, blockListIPWithPort block.ListIPWithPort) error {
|
||||
b.mu.Lock()
|
||||
b.blockListIP = blockListIP
|
||||
b.blockListIPWithPort = blockListIPWithPort
|
||||
b.mu.Unlock()
|
||||
|
||||
isExpiredEntries := false
|
||||
@@ -56,16 +68,29 @@ func (b *blocking) NftReload(blockListIP block.ListIP) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
banSeconds := uint32(0)
|
||||
blockSeconds := uint32(0)
|
||||
if e.ExpireAtUnix > 0 {
|
||||
if e.ExpireAtUnix < nowUnix {
|
||||
isExpiredEntries = true
|
||||
return nil
|
||||
}
|
||||
banSeconds = uint32(e.ExpireAtUnix - nowUnix)
|
||||
blockSeconds = uint32(e.ExpireAtUnix - nowUnix)
|
||||
}
|
||||
|
||||
if err := b.blockListIP.AddIP(ip, banSeconds); err != nil {
|
||||
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.AddIP(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.AddIP(ip, blockSeconds); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to add IP %s to block list: %s", ip.String(), err))
|
||||
return nil
|
||||
}
|
||||
@@ -86,38 +111,117 @@ func (b *blocking) NftReload(blockListIP block.ListIP) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *blocking) BlockIP(blockIP BlockIP) (bool, error) {
|
||||
if blockIP.IP.IsLoopback() {
|
||||
return false, fmt.Errorf("loopback IP address %s cannot be blocked", blockIP.IP.String())
|
||||
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(blockIP.IP, blockIP.TimeSeconds); err != nil {
|
||||
if err := b.blockListIP.AddIP(block.IP, block.TimeSeconds); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
expireAtUnix := int64(0)
|
||||
if blockIP.TimeSeconds > 0 {
|
||||
expire := time.Now().Add(time.Duration(int64(blockIP.TimeSeconds)) * time.Second)
|
||||
if block.TimeSeconds > 0 {
|
||||
expire := time.Now().Add(time.Duration(int64(block.TimeSeconds)) * time.Second)
|
||||
expireAtUnix = expire.Unix()
|
||||
}
|
||||
data := entity.Blocking{
|
||||
IP: blockIP.IP.String(),
|
||||
IP: block.IP.String(),
|
||||
ExpireAtUnix: expireAtUnix,
|
||||
Reason: blockIP.Reason,
|
||||
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", blockIP.IP.String(), err)
|
||||
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
|
||||
@@ -138,3 +242,16 @@ func (b *blocking) UnblockAllIPs() error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package block
|
||||
|
||||
import (
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
)
|
||||
|
||||
type Blocklist interface {
|
||||
// ReplaceElementsIPv4 Replacing IP addresses.
|
||||
ReplaceElementsIPv4(ips []string) error
|
||||
|
||||
// ReplaceElementsIPv6 Replacing IP addresses.
|
||||
ReplaceElementsIPv6(ips []string) error
|
||||
|
||||
// AddRuleToChain Add a rule to the parent chain.
|
||||
AddRuleToChain(chainAddRuleFunc func(expr ...string) error, action string) error
|
||||
}
|
||||
|
||||
type blocklist struct {
|
||||
listIPv4 List
|
||||
listIPv6 List
|
||||
}
|
||||
|
||||
func NewBlocklist(nft nft.NFT, family family.Type, table string, name string) (Blocklist, error) {
|
||||
params := "type ipv4_addr; flags interval; auto-merge;"
|
||||
listName := name + "_ip4"
|
||||
listIPv4, err := newList(nft, family, table, listName, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params = "type ipv6_addr; flags interval; auto-merge;"
|
||||
listName = name + "_ip6"
|
||||
listIPv6, err := newList(nft, family, table, listName, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &blocklist{
|
||||
listIPv4: listIPv4,
|
||||
listIPv6: listIPv6,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *blocklist) ReplaceElementsIPv4(ips []string) error {
|
||||
return l.listIPv4.ReplaceElements(ips)
|
||||
}
|
||||
|
||||
func (l *blocklist) ReplaceElementsIPv6(ips []string) error {
|
||||
return l.listIPv6.ReplaceElements(ips)
|
||||
}
|
||||
|
||||
func (l *blocklist) AddRuleToChain(chainAddRuleFunc func(expr ...string) error, action string) error {
|
||||
rule := "ip saddr @" + l.listIPv4.Name() + " " + action
|
||||
if err := chainAddRuleFunc(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rule = "ip6 saddr @" + l.listIPv6.Name() + " " + action
|
||||
if err := chainAddRuleFunc(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package block
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
@@ -11,6 +12,7 @@ type List interface {
|
||||
Name() string
|
||||
AddElement(element string) error
|
||||
DeleteElement(element string) error
|
||||
ReplaceElements(elements []string) error
|
||||
}
|
||||
|
||||
type list struct {
|
||||
@@ -57,3 +59,43 @@ func (l *list) DeleteElement(element string) error {
|
||||
}
|
||||
return l.nft.Command().Run(command...)
|
||||
}
|
||||
|
||||
func (l *list) ReplaceElements(elements []string) error {
|
||||
if len(elements) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := l.nft.Command().Run("flush set", l.family.String(), l.table, l.name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const batchSize = 200
|
||||
for _, batch := range chunkStrings(elements, batchSize) {
|
||||
command := []string{
|
||||
"add element",
|
||||
l.family.String(), l.table, l.name,
|
||||
fmt.Sprintf("{ %s }", strings.Join(batch, ",")),
|
||||
}
|
||||
if err := l.nft.Command().Run(command...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func chunkStrings(items []string, size int) [][]string {
|
||||
if size <= 0 {
|
||||
size = 100
|
||||
}
|
||||
|
||||
chunks := make([][]string, 0, (len(items)+size-1)/size)
|
||||
for start := 0; start < len(items); start += size {
|
||||
end := start + size
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
chunks = append(chunks, items[start:end])
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
@@ -55,10 +55,10 @@ func (l *listIP) AddIP(addr net.IP, banSeconds uint32) error {
|
||||
element := strings.Join(el, " ")
|
||||
|
||||
if addr.To4() != nil {
|
||||
return l.listIPv4.AddElement(fmt.Sprintf("%s", element))
|
||||
return l.listIPv4.AddElement(element)
|
||||
}
|
||||
|
||||
return l.listIPv6.AddElement(fmt.Sprintf("%s", element))
|
||||
return l.listIPv6.AddElement(element)
|
||||
}
|
||||
|
||||
func (l *listIP) DeleteIP(addr net.IP) error {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package block
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
)
|
||||
|
||||
type ListIPWithPort interface {
|
||||
// AddIP Add an IP address to the list.
|
||||
AddIP(addr net.IP, ports []types.L4Port, banSeconds uint32) error
|
||||
|
||||
// DeleteIP Delete an IP address from the list.
|
||||
DeleteIP(addr net.IP, port types.L4Port) error
|
||||
|
||||
// AddRuleToChain Add a rule to the parent chain.
|
||||
AddRuleToChain(chainAddRuleFunc func(expr ...string) error, action string) error
|
||||
}
|
||||
|
||||
type listIPWithPort struct {
|
||||
listIPv4 List
|
||||
listIPv6 List
|
||||
}
|
||||
|
||||
func NewListIPWithPort(nft nft.NFT, family family.Type, table string, name string) (ListIPWithPort, error) {
|
||||
params := "type ipv4_addr . inet_proto . inet_service; flags interval, timeout;"
|
||||
listName := name + "_ip4"
|
||||
listIPv4, err := newList(nft, family, table, listName, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params = "type ipv6_addr . inet_proto . inet_service; flags interval, timeout;"
|
||||
listName = name + "_ip6"
|
||||
listIPv6, err := newList(nft, family, table, listName, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &listIPWithPort{
|
||||
listIPv4: listIPv4,
|
||||
listIPv6: listIPv6,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *listIPWithPort) AddIP(addr net.IP, ports []types.L4Port, banSeconds uint32) error {
|
||||
if len(ports) == 0 {
|
||||
return fmt.Errorf("ports is empty")
|
||||
}
|
||||
|
||||
var elements []string
|
||||
for _, port := range ports {
|
||||
el := []string{fmt.Sprintf("%s . %s . %d", addr.String(), port.ProtocolString(), port.Number())}
|
||||
if banSeconds > 0 {
|
||||
el = append(el, "timeout", fmt.Sprintf("%ds", banSeconds))
|
||||
}
|
||||
|
||||
elements = append(elements, strings.Join(el, " "))
|
||||
}
|
||||
|
||||
element := strings.Join(elements, ",")
|
||||
if addr.To4() != nil {
|
||||
return l.listIPv4.AddElement(element)
|
||||
}
|
||||
|
||||
return l.listIPv6.AddElement(element)
|
||||
}
|
||||
|
||||
func (l *listIPWithPort) DeleteIP(addr net.IP, port types.L4Port) error {
|
||||
if addr == nil {
|
||||
return fmt.Errorf("IP address cannot be nil")
|
||||
}
|
||||
if port.ToString() == "" {
|
||||
return fmt.Errorf("port cannot be empty")
|
||||
}
|
||||
|
||||
element := fmt.Sprintf("%s . %s . %d", addr.String(), port.ProtocolString(), port.Number())
|
||||
|
||||
if addr.To4() != nil {
|
||||
return l.listIPv4.DeleteElement(element)
|
||||
}
|
||||
|
||||
return l.listIPv6.DeleteElement(element)
|
||||
}
|
||||
|
||||
func (l *listIPWithPort) AddRuleToChain(chainAddRuleFunc func(expr ...string) error, action string) error {
|
||||
rule := "ip saddr . meta l4proto . th dport @" + l.listIPv4.Name() + " " + action
|
||||
if err := chainAddRuleFunc(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rule = "ip6 saddr . meta l4proto . th dport @" + l.listIPv6.Name() + " " + action
|
||||
if err := chainAddRuleFunc(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package block
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
|
||||
)
|
||||
|
||||
func NewPortKnocking(nft nft.NFT, family family.Type, table string, name string, ipVersion ip.Version, timeout uint32) error {
|
||||
params := []string{"type", ipVersion.ToNftForSet() + ";", "flags timeout; timeout", strconv.Itoa(int(timeout)) + "s;"}
|
||||
_, err := newList(nft, family, table, name, strings.Join(params, " "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -42,6 +42,9 @@ type Chains interface {
|
||||
NewNoneChain(chain string) (Chain, error)
|
||||
NewChain(chain string, baseChain nftChain.ChainOptions) (Chain, error)
|
||||
NewBlockListIP(name string) (block.ListIP, error)
|
||||
NewBlockListIPWithPort(name string) (block.ListIPWithPort, error)
|
||||
NewBlocklist(name string) (block.Blocklist, error)
|
||||
NewPortKnocking(name string) (PortKnocking, error)
|
||||
}
|
||||
|
||||
type chains struct {
|
||||
@@ -231,6 +234,37 @@ func (c *chains) NewBlockListIP(name string) (block.ListIP, error) {
|
||||
return blockList, nil
|
||||
}
|
||||
|
||||
func (c *chains) NewBlockListIPWithPort(name string) (block.ListIPWithPort, error) {
|
||||
blockList, err := block.NewListIPWithPort(c.nft, c.family, c.table, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return blockList, nil
|
||||
}
|
||||
|
||||
func (c *chains) NewBlocklist(name string) (block.Blocklist, error) {
|
||||
blockList, err := block.NewBlocklist(c.nft, c.family, c.table, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := blockList.AddRuleToChain(c.afterLocalInput.AddRule, "drop"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return blockList, nil
|
||||
}
|
||||
|
||||
func (c *chains) NewPortKnocking(name string) (PortKnocking, error) {
|
||||
portKnocking, err := newPortKnocking(c.nft, c.family, c.table, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return portKnocking, 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) {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain/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/pkg/ip"
|
||||
)
|
||||
|
||||
type PortKnocking interface {
|
||||
AddFirstStageRule(
|
||||
name string,
|
||||
ipVersion ip.Version,
|
||||
l4Port types.L4Port,
|
||||
timeout uint32,
|
||||
action types.KnockAction,
|
||||
) error
|
||||
AddNextStageRule(
|
||||
prevName, nextName string,
|
||||
ipVersion ip.Version,
|
||||
l4Port types.L4Port,
|
||||
timeout uint32,
|
||||
action types.KnockAction,
|
||||
) error
|
||||
AddRuleIn(AddRuleFunc func(expr ...string) error) error
|
||||
}
|
||||
|
||||
type portKnocking struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
chain string
|
||||
}
|
||||
|
||||
func newPortKnocking(nft nft.NFT, family family.Type, table string, chain string) (PortKnocking, error) {
|
||||
if err := nft.Chain().Add(family, table, chain, nftChain.TypeNone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &portKnocking{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
chain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *portKnocking) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
|
||||
return AddRuleFunc("iifname != \"lo\" counter jump " + k.chain)
|
||||
}
|
||||
|
||||
func (k *portKnocking) AddFirstStageRule(
|
||||
name string,
|
||||
ipVersion ip.Version,
|
||||
l4Port types.L4Port,
|
||||
timeout uint32,
|
||||
action types.KnockAction,
|
||||
) error {
|
||||
if err := block.NewPortKnocking(k.nft, k.family, k.table, name, ipVersion, timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expr := []string{
|
||||
l4Port.ProtocolString(), "dport", l4Port.NumberString(), "add", "@" + name,
|
||||
"{", ipVersion.ToNft(), "saddr timeout", strconv.Itoa(int(timeout)) + "s", "}", action.String(),
|
||||
}
|
||||
return k.nft.Rule().Add(k.family, k.table, k.chain, expr...)
|
||||
}
|
||||
|
||||
func (k *portKnocking) AddNextStageRule(
|
||||
prevName, nextName string,
|
||||
ipVersion ip.Version,
|
||||
l4Port types.L4Port,
|
||||
timeout uint32,
|
||||
action types.KnockAction,
|
||||
) error {
|
||||
if err := block.NewPortKnocking(k.nft, k.family, k.table, nextName, ipVersion, timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expr := []string{
|
||||
ipVersion.ToNft(), "saddr", "@" + prevName,
|
||||
l4Port.ProtocolString(), "dport", l4Port.NumberString(), "add", "@" + nextName,
|
||||
"{", ipVersion.ToNft(), "saddr}", action.String(),
|
||||
}
|
||||
return k.nft.Rule().Add(k.family, k.table, k.chain, expr...)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package firewall
|
||||
|
||||
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,6 +13,7 @@ type Config struct {
|
||||
Options ConfigOptions
|
||||
MetadataNaming ConfigMetadata
|
||||
Policy ConfigPolicy
|
||||
PortKnocking []ConfigPortKnocking
|
||||
}
|
||||
|
||||
type ConfigOptions struct {
|
||||
@@ -35,32 +37,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 +63,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
|
||||
|
||||
@@ -2,8 +2,10 @@ package firewall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"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/blocking"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
|
||||
@@ -25,9 +27,15 @@ type API interface {
|
||||
// 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
|
||||
|
||||
@@ -42,9 +50,17 @@ type firewall struct {
|
||||
blockingService blocking.API
|
||||
chains chain.Chains
|
||||
docker docker_monitor.Docker
|
||||
blocklist blocklist.Blocklist
|
||||
}
|
||||
|
||||
func New(pathNFT string, blockingService blocking.API, logger log.Logger, config Config, docker docker_monitor.Docker) (API, error) {
|
||||
func New(
|
||||
pathNFT string,
|
||||
blockingService blocking.API,
|
||||
logger log.Logger,
|
||||
config Config,
|
||||
docker docker_monitor.Docker,
|
||||
blocklist blocklist.Blocklist,
|
||||
) (API, error) {
|
||||
nft, err := nftables.NewWithPath(pathNFT)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create nft client: %w %s", err, pathNFT)
|
||||
@@ -56,6 +72,7 @@ func New(pathNFT string, blockingService blocking.API, logger log.Logger, config
|
||||
config: &config,
|
||||
blockingService: blockingService,
|
||||
docker: docker,
|
||||
blocklist: blocklist,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -99,6 +116,10 @@ func (f *firewall) Reload() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.blocklist.NftReload(f.chains.NewBlocklist); err != nil {
|
||||
f.logger.Error(fmt.Sprintf("Failed to reload blocklist: %s", err))
|
||||
}
|
||||
|
||||
f.logger.Debug("Reload nftables rules done")
|
||||
return nil
|
||||
}
|
||||
@@ -126,6 +147,10 @@ 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()
|
||||
}
|
||||
@@ -167,6 +192,15 @@ func (f *firewall) BlockIP(blockIP blocking.BlockIP) (bool, error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package firewall
|
||||
|
||||
func (f *firewall) reloadBlockList() error {
|
||||
listBan, err := f.chains.NewBlockListIP("ban")
|
||||
listBlockedIP, err := f.chains.NewBlockListIP("blocked_ip")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := listBan.AddRuleToChain(f.chains.BeforeLocalInput().AddRule, "drop"); err != nil {
|
||||
if err := listBlockedIP.AddRuleToChain(f.chains.BeforeLocalInput().AddRule, "drop"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.blockingService.NftReload(listBan); err != nil {
|
||||
listBlockedIPWithPort, err := f.chains.NewBlockListIPWithPort("blocked_ip_port")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := listBlockedIPWithPort.AddRuleToChain(f.chains.BeforeLocalInput().AddRule, "drop"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.blockingService.NftReload(listBlockedIP, listBlockedIPWithPort); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package firewall
|
||||
|
||||
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
|
||||
func (f *firewall) reloadForward() error {
|
||||
f.logger.Debug("Reloading forward chain")
|
||||
err := f.chains.NewForward(f.config.MetadataNaming.ChainForwardName, f.config.Policy.DefaultAllowForward, f.config.Policy.ForwardPriority)
|
||||
@@ -38,7 +40,7 @@ func (f *firewall) reloadForwardAddIPs() error {
|
||||
}
|
||||
|
||||
for _, ipConfig := range f.config.IP4.InIPs {
|
||||
if ipConfig.Action != ActionDrop && ipConfig.Action != ActionReject {
|
||||
if ipConfig.Action != types.ActionDrop && ipConfig.Action != types.ActionReject {
|
||||
continue
|
||||
}
|
||||
if err := forwardAddIP(chain.AddRule, ipConfig, "ip"); err != nil {
|
||||
@@ -51,7 +53,7 @@ func (f *firewall) reloadForwardAddIPs() error {
|
||||
}
|
||||
|
||||
for _, ipConfig := range f.config.IP6.InIPs {
|
||||
if ipConfig.Action != ActionDrop && ipConfig.Action != ActionReject {
|
||||
if ipConfig.Action != types.ActionDrop && ipConfig.Action != types.ActionReject {
|
||||
continue
|
||||
}
|
||||
if err := forwardAddIP(chain.AddRule, ipConfig, "ip6"); err != nil {
|
||||
|
||||
@@ -3,8 +3,8 @@ package firewall
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"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/pkg"
|
||||
)
|
||||
|
||||
@@ -215,8 +215,8 @@ func (f *firewall) reloadInputICMP6Strict() error {
|
||||
func (f *firewall) reloadInputPorts() error {
|
||||
chain := f.chains.Input()
|
||||
for _, port := range f.config.InPorts {
|
||||
protocol := port.Protocol.String()
|
||||
number := strconv.Itoa(int(port.Number))
|
||||
protocol := port.Port.ProtocolString()
|
||||
number := port.Port.NumberString()
|
||||
|
||||
baseRule := "iifname != \"lo\" meta l4proto " + protocol + " ct state new " + protocol + " dport " + number
|
||||
|
||||
@@ -248,6 +248,10 @@ func (f *firewall) reloadInputAddIPs() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.reloadPortKnocking(chain); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ipConfig := range f.config.IP4.InIPs {
|
||||
if err := inputAddIP(chain.AddRule, ipConfig, "ip"); err != nil {
|
||||
return err
|
||||
@@ -266,11 +270,54 @@ func (f *firewall) reloadInputAddIPs() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *firewall) reloadPortKnocking(chain chain.LocalInput) error {
|
||||
if len(f.config.PortKnocking) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
portKnocking, err := f.chains.NewPortKnocking("port_knocking")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, portKnockingConfig := range f.config.PortKnocking {
|
||||
var knockName, prevKnockName string
|
||||
for index, knock := range portKnockingConfig.Knocks {
|
||||
prevKnockName = knockName
|
||||
knockName = fmt.Sprintf("knock_%s_%d", portKnockingConfig.Name, index)
|
||||
if index == 0 {
|
||||
if err := portKnocking.AddFirstStageRule(knockName, portKnockingConfig.IPVersion, knock.Port, knock.Timeout, knock.Action); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := portKnocking.AddNextStageRule(prevKnockName, knockName, portKnockingConfig.IPVersion, knock.Port, knock.Timeout, knock.Action); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
expr := []string{
|
||||
portKnockingConfig.IPVersion.ToNft(), "saddr", "@" + knockName,
|
||||
portKnockingConfig.Port.ProtocolString(), "dport", portKnockingConfig.Port.NumberString(), "accept",
|
||||
}
|
||||
if err := chain.AddRule(expr...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := portKnocking.AddRuleIn(chain.AddRule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func inputAddIP(addRuleFunc func(expr ...string) error, config ConfigIP, ipMatch string) error {
|
||||
|
||||
rule := ipMatch + " saddr " + config.IP + " iifname != \"lo\""
|
||||
if !config.OnlyIP {
|
||||
rule += " " + config.Protocol.String() + " dport " + strconv.Itoa(int(config.Port))
|
||||
rule += " " + config.Port.ProtocolString() + " dport " + config.Port.NumberString()
|
||||
}
|
||||
if config.LimitRate != "" {
|
||||
rule += " limit rate " + config.LimitRate
|
||||
|
||||
@@ -3,7 +3,6 @@ package firewall
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg"
|
||||
)
|
||||
@@ -176,8 +175,8 @@ func (f *firewall) reloadOutputICMPAfter() error {
|
||||
func (f *firewall) reloadOutputPorts() error {
|
||||
chain := f.chains.Output()
|
||||
for _, port := range f.config.OutPorts {
|
||||
protocol := port.Protocol.String()
|
||||
number := strconv.Itoa(int(port.Number))
|
||||
protocol := port.Port.ProtocolString()
|
||||
number := port.Port.NumberString()
|
||||
baseRule := "oifname != \"lo\" meta l4proto " + protocol + " ct state new " + protocol + " dport " + number
|
||||
|
||||
if port.LimitRate != "" {
|
||||
@@ -231,7 +230,7 @@ func outputAddIP(addRuleFunc func(expr ...string) error, config ConfigIP, ipMatc
|
||||
|
||||
rule := ipMatch + " daddr " + config.IP + " oifname != \"lo\""
|
||||
if !config.OnlyIP {
|
||||
rule += " " + config.Protocol.String() + " dport " + strconv.Itoa(int(config.Port))
|
||||
rule += " " + config.Port.ProtocolString() + " dport " + config.Port.NumberString()
|
||||
}
|
||||
if config.LimitRate != "" {
|
||||
rule += " limit rate " + config.LimitRate
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type L4Port interface {
|
||||
Number() uint16
|
||||
NumberString() string
|
||||
ProtocolString() string
|
||||
ToString() string
|
||||
}
|
||||
|
||||
type l4Port struct {
|
||||
number uint16
|
||||
protocol string
|
||||
}
|
||||
|
||||
func NewL4Port(number uint16, protocol Protocol) (L4Port, error) {
|
||||
if protocol != ProtocolTCP && protocol != ProtocolUDP {
|
||||
return nil, errors.New("invalid protocol")
|
||||
}
|
||||
|
||||
return &l4Port{number: number, protocol: protocol.String()}, nil
|
||||
}
|
||||
|
||||
func (p *l4Port) Number() uint16 {
|
||||
return p.number
|
||||
}
|
||||
|
||||
func (p *l4Port) NumberString() string {
|
||||
port := p.Number()
|
||||
return strconv.Itoa(int(port))
|
||||
}
|
||||
|
||||
func (p *l4Port) ProtocolString() string {
|
||||
return p.protocol
|
||||
}
|
||||
|
||||
func (p *l4Port) ToString() string {
|
||||
return p.NumberString() + "/" + p.ProtocolString()
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
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 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 KnockAction int8
|
||||
|
||||
const (
|
||||
KnockActionAccept KnockAction = iota + 1
|
||||
KnockActionReject
|
||||
KnockActionDrop
|
||||
KnockActionReturn
|
||||
)
|
||||
|
||||
func (a KnockAction) String() string {
|
||||
switch a {
|
||||
case KnockActionAccept:
|
||||
return "accept"
|
||||
case KnockActionReject:
|
||||
return "reject"
|
||||
case KnockActionDrop:
|
||||
return "drop"
|
||||
case KnockActionReturn:
|
||||
return "return"
|
||||
default:
|
||||
return "drop"
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"errors"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection_group"
|
||||
"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"
|
||||
firewall2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/blocking"
|
||||
@@ -13,7 +15,13 @@ import (
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
func NewDaemon(opts DaemonOptions, logger log.Logger, notifications notifications.Notifications, docker docker_monitor.Docker) (Daemon, error) {
|
||||
func NewDaemon(
|
||||
opts DaemonOptions,
|
||||
logger log.Logger,
|
||||
notifications notifications.Notifications,
|
||||
docker docker_monitor.Docker,
|
||||
blocklist blocklist.Blocklist,
|
||||
) (Daemon, error) {
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
@@ -29,9 +37,17 @@ func NewDaemon(opts DaemonOptions, logger log.Logger, notifications notification
|
||||
}
|
||||
|
||||
blockingService := blocking.New(opts.Repositories.Blocking(), logger)
|
||||
firewall, err := firewall2.New(opts.PathNftables, blockingService, logger, opts.ConfigFirewall, docker)
|
||||
firewall, err := firewall2.New(
|
||||
opts.PathNftables,
|
||||
blockingService,
|
||||
logger,
|
||||
opts.ConfigFirewall,
|
||||
docker,
|
||||
blocklist,
|
||||
)
|
||||
|
||||
analyzerService := analyzer.New(opts.ConfigAnalyzer, firewall.BlockIP, opts.Repositories, logger, notifications)
|
||||
blockService := brute_force_protection_group.NewBlockService(firewall.BlockIP, firewall.BlockIPWithPorts)
|
||||
analyzerService := analyzer.New(opts.ConfigAnalyzer, blockService, opts.Repositories, logger, notifications)
|
||||
|
||||
return &daemon{
|
||||
pidFile: pidFile,
|
||||
@@ -41,5 +57,6 @@ func NewDaemon(opts DaemonOptions, logger log.Logger, notifications notification
|
||||
notifications: notifications,
|
||||
analyzer: analyzerService,
|
||||
docker: docker,
|
||||
blocklist: blocklist,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package socket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -11,7 +12,12 @@ import (
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type HandleCommand func(command string, socket Connect) error
|
||||
type Message struct {
|
||||
Command string `json:"command"`
|
||||
Args map[string]string `json:"args"`
|
||||
}
|
||||
|
||||
type HandleCommand func(command string, args map[string]string, socket Connect) error
|
||||
|
||||
type Socket interface {
|
||||
EnsureNoOtherProcess() error
|
||||
@@ -121,13 +127,19 @@ func (s *socket) handleAction(conn net.Conn, handleCommand HandleCommand) {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
cmd, err := sock.Read()
|
||||
raw, err := sock.Read()
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to read command: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := handleCommand(cmd, sock); err != nil {
|
||||
cmd, args, err := parseCommand(raw)
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to parse command: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := handleCommand(cmd, args, sock); err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to handle command: %s", err))
|
||||
}
|
||||
}
|
||||
@@ -147,3 +159,21 @@ func canConnect(path string) bool {
|
||||
func isUseOfClosedNetworkError(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "use of closed network connection")
|
||||
}
|
||||
|
||||
func parseCommand(raw string) (string, map[string]string, error) {
|
||||
var msg Message
|
||||
|
||||
if err := json.Unmarshal([]byte(raw), &msg); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if msg.Command == "" {
|
||||
return "", nil, errors.New("command is empty")
|
||||
}
|
||||
|
||||
if msg.Args == nil {
|
||||
msg.Args = map[string]string{}
|
||||
}
|
||||
|
||||
return msg.Command, msg.Args, nil
|
||||
}
|
||||
|
||||
@@ -25,11 +25,22 @@
|
||||
"notifications_queue_clear_error": "Failed to clear notification queue",
|
||||
"notifications_queue_clear_success": "The notification queue has been cleared.",
|
||||
|
||||
"cmd.daemon.ban.Usage": "Ban",
|
||||
"cmd.daemon.ban.clear.Usage": "Unban all banned IP addresses",
|
||||
"cmd.daemon.ban.clear.Description": "Unban all banned IP addresses.",
|
||||
"ban_clear_error": "Unable to unban all IP addresses",
|
||||
"ban_clear_success": "The request was successfully completed",
|
||||
"cmd.daemon.block.Usage": "Blocking",
|
||||
"cmd.daemon.block.clear.Usage": "Unblock all banned IP addresses",
|
||||
"cmd.daemon.block.clear.Description": "Unblock all banned IP addresses.",
|
||||
"block_clear_error": "Unable to unblock all IP addresses",
|
||||
"block_clear_success": "The request was successfully completed",
|
||||
|
||||
"cmd.daemon.block.add.Usage": "Add IP address to block list",
|
||||
"cmd.daemon.block.add.Description": "Add an IP address to the block list. \nExamples: \nkor-elf-shield block add 192.168.1.1 \nkor-elf-shield block add 192.168.1.1 --seconds=600 \nkor-elf-shield block add 192.168.1.1 --port 80/tcp",
|
||||
"cmd.daemon.block.add.FlagUsage.port": "The port to be blocked. If not specified, all ports will be blocked. \nExamples: \n--port=80/tcp \n--port=1000/udp",
|
||||
"cmd.daemon.block.add.FlagUsage.seconds": "The blocking time in seconds. If not specified, the blocking will be permanent.",
|
||||
"cmd.daemon.block.add.FlagUsage.reason": "Reason for blocking.",
|
||||
"block_add_ip_success": "The IP address has been successfully added to the block list.",
|
||||
|
||||
"cmd.daemon.block.delete.Usage": "Remove IP address from block list",
|
||||
"cmd.daemon.block.delete.Description": "Remove an IP address from the block list. \nExample: \nkor-elf-shield block delete 192.168.1.1",
|
||||
"block_delete_ip_success": "The IP address has been successfully removed from the block list.",
|
||||
|
||||
"Command error": "Command error",
|
||||
"invalid log level": "The log level specified in the settings is invalid. It is currently set to: {{.Level}}. Valid values: {{.Levels}}",
|
||||
@@ -49,6 +60,7 @@
|
||||
"access to user has been gained": "Access to user has been gained",
|
||||
"unknown": "unknown",
|
||||
"blockSec": "Blocked for {{.BlockSec}} seconds",
|
||||
"ports": "Ports: {{.Ports}}",
|
||||
|
||||
"alert.subject": "Alert detected ({{.Name}}) (group:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "Logged into the OS via ssh.",
|
||||
@@ -60,5 +72,7 @@
|
||||
"alert.bruteForceProtection.subject-error": "A hacking attempt was detected, but the IP {{.IP}} is not blocked. Alert ({{.Name}}) (group:{{.GroupName}})",
|
||||
"alert.bruteForceProtection.error": "Error: {{.Error}}",
|
||||
"alert.bruteForceProtection.ssh.message": "An attempt to brute-force SSH was detected.",
|
||||
"alert.bruteForceProtection.group._default.message": "Default group."
|
||||
"alert.bruteForceProtection.group._default.message": "Default group.",
|
||||
|
||||
"cmd.error": "Command error: {{.Error}}"
|
||||
}
|
||||
|
||||
@@ -25,11 +25,22 @@
|
||||
"notifications_queue_clear_error": "Хабарландыру кезегі тазаланбады",
|
||||
"notifications_queue_clear_success": "Хабарландыру кезегі тазартылды",
|
||||
|
||||
"cmd.daemon.ban.Usage": "Тыйым салу",
|
||||
"cmd.daemon.ban.clear.Usage": "Барлық тыйым салынған IP мекенжайларын алып тастау",
|
||||
"cmd.daemon.ban.clear.Description": "Барлық тыйым салынған IP мекенжайларын алып тастау.",
|
||||
"ban_clear_error": "Барлық IP мекенжайларын бұғаттаудан шығару мүмкін емес",
|
||||
"ban_clear_success": "Сұраныс сәтті орындалды",
|
||||
"cmd.daemon.block.Usage": "Бұғаттау",
|
||||
"cmd.daemon.block.clear.Usage": "Барлық тыйым салынған IP мекенжайларын бұғаттан шығарыңыз",
|
||||
"cmd.daemon.block.clear.Description": "Барлық тыйым салынған IP мекенжайларын бұғаттан шығарыңыз.",
|
||||
"block_clear_error": "Барлық IP мекенжайларын бұғаттан шығару мүмкін емес",
|
||||
"block_clear_success": "Сұраныс сәтті орындалды",
|
||||
|
||||
"cmd.daemon.block.add.Usage": "Блоктау тізіміне IP мекенжайын қосу",
|
||||
"cmd.daemon.block.add.Description": "Блоктау тізіміне IP мекенжайын қосыңыз. \nМысалдар: \nkor-elf-shield block add 192.168.1.1 \nkor-elf-shield block add 192.168.1.1 --seconds=600 \nkor-elf-shield block add 192.168.1.1 --port 80/tcp",
|
||||
"cmd.daemon.block.add.FlagUsage.port": "Блокталатын порт. Егер көрсетілмесе, барлық порттар бұғатталады. \nМысалдар: \n--port=80/tcp \n--port=1000/udp",
|
||||
"cmd.daemon.block.add.FlagUsage.seconds": "Блоктау уақыты секундпен. Егер көрсетілмесе, блоктау тұрақты болады.",
|
||||
"cmd.daemon.block.add.FlagUsage.reason": "Блоктау себебі.",
|
||||
"block_add_ip_success": "IP мекенжайы блоктау тізіміне сәтті қосылды.",
|
||||
|
||||
"cmd.daemon.block.delete.Usage": "IP мекенжайын блоктау тізімінен алып тастаңыз",
|
||||
"cmd.daemon.block.delete.Description": "IP мекенжайын блоктау тізімінен алып тастаңыз. \nМысал: \nkor-elf-shield block delete 192.168.1.1",
|
||||
"block_delete_ip_success": "IP мекенжайы блоктау тізімінен сәтті жойылды.",
|
||||
|
||||
"Command error": "Командалық қате",
|
||||
"invalid log level": "Параметрлерде көрсетілген журнал деңгейі жарамсыз. Ол қазір мына күйге орнатылған: {{.Level}}. Жарамды мәндер: {{.Levels}}",
|
||||
@@ -49,6 +60,7 @@
|
||||
"access to user has been gained": "Пайдаланушыға кіру мүмкіндігі алынды",
|
||||
"unknown": "белгісіз",
|
||||
"blockSec": "{{.BlockSec}} секундқа блокталды",
|
||||
"ports": "Порттар: {{.Ports}}",
|
||||
|
||||
"alert.subject": "Ескерту анықталды ({{.Name}}) (топ:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "ОС-қа ssh арқылы кірді.",
|
||||
@@ -60,5 +72,7 @@
|
||||
"alert.bruteForceProtection.subject-error": "Хакерлік әрекет анықталды, бірақ IP мекенжайы {{.IP}} бұғатталмаған. Ескерту ({{.Name}}) (Топ:{{.GroupName}})",
|
||||
"alert.bruteForceProtection.error": "Қате: {{.Error}}",
|
||||
"alert.bruteForceProtection.ssh.message": "SSH-ті күштеп қолдану әрекеті анықталды.",
|
||||
"alert.bruteForceProtection.group._default.message": "Әдепкі топ."
|
||||
"alert.bruteForceProtection.group._default.message": "Әдепкі топ.",
|
||||
|
||||
"cmd.error": "Команда қатесі: {{.Error}}"
|
||||
}
|
||||
@@ -25,11 +25,22 @@
|
||||
"notifications_queue_clear_error": "Не удалось очистить очередь уведомлений",
|
||||
"notifications_queue_clear_success": "Очередь уведомлений очищена",
|
||||
|
||||
"cmd.daemon.ban.Usage": "Бан",
|
||||
"cmd.daemon.ban.clear.Usage": "Разбанить все забаненные IP адреса",
|
||||
"cmd.daemon.ban.clear.Description": "Разбанить все забаненные IP адреса.",
|
||||
"ban_clear_error": "Не смогли разбанить все IP адреса",
|
||||
"ban_clear_success": "Запрос успешно выполнен",
|
||||
"cmd.daemon.block.Usage": "Блокировка",
|
||||
"cmd.daemon.block.clear.Usage": "Разблокировать все забаненные IP адреса",
|
||||
"cmd.daemon.block.clear.Description": "блокировка все забаненные IP адреса.",
|
||||
"block_clear_error": "Не смогли разблокировать все IP адреса",
|
||||
"block_clear_success": "Запрос успешно выполнен",
|
||||
|
||||
"cmd.daemon.block.add.Usage": "Добавить IP адрес в список заблокированных",
|
||||
"cmd.daemon.block.add.Description": "Добавить IP адрес в список заблокированных. \nПримеры: \nkor-elf-shield block add 192.168.1.1 \nkor-elf-shield block add 192.168.1.1 --seconds=600 \nkor-elf-shield block add 192.168.1.1 --port 80/tcp",
|
||||
"cmd.daemon.block.add.FlagUsage.port": "Порт, который будет заблокирован. Если не указать, то заблокируются все порты. \nПримеры: \n--port=80/tcp \n--port=1000/udp",
|
||||
"cmd.daemon.block.add.FlagUsage.seconds": "Время блокировки в секундах. Если не указать, то блокировка будет вечной.",
|
||||
"cmd.daemon.block.add.FlagUsage.reason": "Причина блокировки.",
|
||||
"block_add_ip_success": "IP адрес успешно добавлен в список заблокированных.",
|
||||
|
||||
"cmd.daemon.block.delete.Usage": "Удалить IP адрес из списка заблокированных",
|
||||
"cmd.daemon.block.delete.Description": "Удалить IP адрес из списка заблокированных. \nПример: \nkor-elf-shield block delete 192.168.1.1",
|
||||
"block_delete_ip_success": "IP адрес успешно удален из списка заблокированных.",
|
||||
|
||||
"Command error": "Ошибка команды",
|
||||
"invalid log level": "В настройках указан не верный уровень log. Сейчас указан: {{.Level}}. Допустимые значения: {{.Levels}}",
|
||||
@@ -49,6 +60,7 @@
|
||||
"access to user has been gained": "Получен доступ к пользователю",
|
||||
"unknown": "неизвестный",
|
||||
"blockSec": "Блокировка на {{.BlockSec}} секунд",
|
||||
"ports": "Порты: {{.Ports}}",
|
||||
|
||||
"alert.subject": "Обнаружено оповещение ({{.Name}}) (группа:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "Вошли в ОС через ssh.",
|
||||
@@ -60,5 +72,7 @@
|
||||
"alert.bruteForceProtection.subject-error": "Обнаружена попытка взлома, но IP {{.IP}} не заблокирован. Оповещение ({{.Name}}) (группа:{{.GroupName}})",
|
||||
"alert.bruteForceProtection.error": "Ошибка: {{.Error}}",
|
||||
"alert.bruteForceProtection.ssh.message": "Обнаружена попытка атаки на SSH методом перебора паролей.",
|
||||
"alert.bruteForceProtection.group._default.message": "Группа по умолчанию."
|
||||
"alert.bruteForceProtection.group._default.message": "Группа по умолчанию.",
|
||||
|
||||
"cmd.error": "Ошибка команды: {{.Error}}"
|
||||
}
|
||||
@@ -23,6 +23,17 @@ func (v Version) ToNft() string {
|
||||
}
|
||||
}
|
||||
|
||||
func (v Version) ToNftForSet() string {
|
||||
switch v {
|
||||
case IPv4:
|
||||
return "ipv4_addr"
|
||||
case IPv6:
|
||||
return "ipv6_addr"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func DetermineIPVersion(ip string) (ipNet string, version Version, err error) {
|
||||
ipNet, version, err = parseCIDR(ip)
|
||||
if err != nil {
|
||||
|
||||
+29
-14
@@ -4,40 +4,55 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
)
|
||||
|
||||
func ToDirection(direction string) (firewall.Direction, error) {
|
||||
func ToDirection(direction string) (types.Direction, error) {
|
||||
switch strings.ToLower(direction) {
|
||||
case "in":
|
||||
return firewall.DirectionIn, nil
|
||||
return types.DirectionIn, nil
|
||||
case "out":
|
||||
return firewall.DirectionOut, nil
|
||||
return types.DirectionOut, nil
|
||||
default:
|
||||
return firewall.DirectionIn, errors.New("invalid direction. Must be in or out")
|
||||
return types.DirectionIn, errors.New("invalid direction. Must be in or out")
|
||||
}
|
||||
}
|
||||
|
||||
func ToProtocol(protocol string) (firewall.Protocol, error) {
|
||||
func ToProtocol(protocol string) (types.Protocol, error) {
|
||||
switch strings.ToLower(protocol) {
|
||||
case "tcp":
|
||||
return firewall.ProtocolTCP, nil
|
||||
return types.ProtocolTCP, nil
|
||||
case "udp":
|
||||
return firewall.ProtocolUDP, nil
|
||||
return types.ProtocolUDP, nil
|
||||
default:
|
||||
return firewall.ProtocolTCP, errors.New("invalid protocol. Must be tcp or udp")
|
||||
return types.ProtocolTCP, errors.New("invalid protocol. Must be tcp or udp")
|
||||
}
|
||||
}
|
||||
|
||||
func ToAction(action string) (firewall.Action, error) {
|
||||
func ToAction(action string) (types.Action, error) {
|
||||
switch strings.ToLower(action) {
|
||||
case "accept":
|
||||
return firewall.ActionAccept, nil
|
||||
return types.ActionAccept, nil
|
||||
case "drop":
|
||||
return firewall.ActionDrop, nil
|
||||
return types.ActionDrop, nil
|
||||
case "reject":
|
||||
return firewall.ActionReject, nil
|
||||
return types.ActionReject, nil
|
||||
default:
|
||||
return firewall.ActionAccept, errors.New("invalid action. Must be accept, drop or reject")
|
||||
return types.ActionAccept, errors.New("invalid action. Must be accept, drop or reject")
|
||||
}
|
||||
}
|
||||
|
||||
func ToKnockAction(action string) (types.KnockAction, error) {
|
||||
switch strings.ToLower(action) {
|
||||
case "accept":
|
||||
return types.KnockActionAccept, nil
|
||||
case "drop":
|
||||
return types.KnockActionDrop, nil
|
||||
case "reject":
|
||||
return types.KnockActionReject, nil
|
||||
case "return":
|
||||
return types.KnockActionReturn, nil
|
||||
default:
|
||||
return types.KnockActionDrop, errors.New("invalid action. Must be accept, return, drop or reject")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ package analyzer
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"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/i18n"
|
||||
"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 BruteForceProtection struct {
|
||||
@@ -123,6 +128,7 @@ func (p *BruteForceProtection) groups() (map[string]*brute_force_protection.Grou
|
||||
Count: uint32(p.RateLimitCount),
|
||||
Period: uint32(p.RateLimitPeriod),
|
||||
BlockingTimeSeconds: uint32(p.BlockingTime),
|
||||
BlockConfig: brute_force_protection.NewBlockOnceIPConfig(),
|
||||
},
|
||||
},
|
||||
RateLimitResetPeriod: uint32(p.RateLimitResetPeriod),
|
||||
@@ -138,3 +144,56 @@ func (p *BruteForceProtection) groups() (map[string]*brute_force_protection.Grou
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func toBlockConfigBySettings(blockType string, ports []string) (brute_force_protection.Block, error) {
|
||||
if blockType == "" {
|
||||
return nil, errors.New("block type is empty")
|
||||
}
|
||||
|
||||
switch blockType {
|
||||
case "ip":
|
||||
return brute_force_protection.NewBlockOnceIPConfig(), nil
|
||||
case "ip_port":
|
||||
if len(ports) == 0 {
|
||||
return nil, errors.New("ports is empty")
|
||||
}
|
||||
|
||||
var blockPorts []types.L4Port
|
||||
for _, port := range ports {
|
||||
l4Port, err := toL4Port(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockPorts = append(blockPorts, l4Port)
|
||||
}
|
||||
|
||||
return brute_force_protection.NewBlockIPAndPortsConfig(blockPorts), nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown block type")
|
||||
}
|
||||
|
||||
func toL4Port(portString string) (types.L4Port, error) {
|
||||
if portString == "" {
|
||||
return nil, errors.New("port is empty")
|
||||
}
|
||||
|
||||
data := strings.Split(portString, "/")
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ type BruteForceProtectionGroup struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Message string `mapstructure:"message"`
|
||||
RateLimitResetPeriod int `mapstructure:"rate_limit_reset_period"`
|
||||
BlockType string `mapstructure:"block_type"`
|
||||
Ports []string `mapstructure:"ports"`
|
||||
RateLimits []BruteForceProtectionGroupRateLimit `mapstructure:"rate_limits"`
|
||||
}
|
||||
|
||||
@@ -20,8 +22,17 @@ func (g *BruteForceProtectionGroup) ToGroup() (*brute_force_protection.Group, er
|
||||
|
||||
var rateLimits []brute_force_protection.RateLimit
|
||||
|
||||
blockType := g.BlockType
|
||||
if blockType == "" {
|
||||
blockType = "ip"
|
||||
}
|
||||
blockConfig, err := toBlockConfigBySettings(blockType, g.Ports)
|
||||
if err := err; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rateLimit := range g.RateLimits {
|
||||
rLimit, err := rateLimit.ToRateLimit()
|
||||
rLimit, err := rateLimit.ToRateLimit(blockConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -7,20 +7,35 @@ import (
|
||||
)
|
||||
|
||||
type BruteForceProtectionGroupRateLimit struct {
|
||||
Count int `mapstructure:"count"`
|
||||
Period int `mapstructure:"period"`
|
||||
BlockingTime int `mapstructure:"blocking_time"`
|
||||
Count int `mapstructure:"count"`
|
||||
Period int `mapstructure:"period"`
|
||||
BlockingTime int `mapstructure:"blocking_time"`
|
||||
BlockType string `mapstructure:"block_type"`
|
||||
Ports []string `mapstructure:"ports"`
|
||||
}
|
||||
|
||||
func (l *BruteForceProtectionGroupRateLimit) ToRateLimit() (brute_force_protection.RateLimit, error) {
|
||||
func (l *BruteForceProtectionGroupRateLimit) ToRateLimit(blockConfig brute_force_protection.Block) (brute_force_protection.RateLimit, error) {
|
||||
if err := l.validate(); err != nil {
|
||||
return brute_force_protection.RateLimit{}, err
|
||||
}
|
||||
|
||||
var rateLimitBlockConfig brute_force_protection.Block
|
||||
|
||||
if l.BlockType != "" {
|
||||
var err error
|
||||
rateLimitBlockConfig, err = toBlockConfigBySettings(l.BlockType, l.Ports)
|
||||
if err != nil {
|
||||
return brute_force_protection.RateLimit{}, err
|
||||
}
|
||||
} else {
|
||||
rateLimitBlockConfig = blockConfig
|
||||
}
|
||||
|
||||
return brute_force_protection.RateLimit{
|
||||
Count: uint32(l.Count),
|
||||
Period: uint32(l.Period),
|
||||
BlockingTimeSeconds: uint32(l.BlockingTime),
|
||||
BlockConfig: rateLimitBlockConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package blocklists
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/blocklist"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Sources []Sources
|
||||
}
|
||||
|
||||
func InitSetting(path string) (Setting, error) {
|
||||
if err := validate.IsTomlFile(path, "otherSettingsPath.blocklists"); err != nil {
|
||||
return Setting{}, err
|
||||
}
|
||||
|
||||
setting := settingDefault()
|
||||
|
||||
v := viper.New()
|
||||
v.SetConfigType("toml")
|
||||
v.SetConfigFile(path)
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return Setting{}, err
|
||||
}
|
||||
if err := v.Unmarshal(&setting); err != nil {
|
||||
return Setting{}, err
|
||||
}
|
||||
|
||||
if !setting.Enabled {
|
||||
return setting, nil
|
||||
}
|
||||
|
||||
return setting, nil
|
||||
}
|
||||
|
||||
func settingDefault() Setting {
|
||||
return Setting{
|
||||
Enabled: false,
|
||||
Sources: []Sources{},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Setting) ToSources(logger log.Logger) []*blocklist.SourceConfig {
|
||||
var sources []*blocklist.SourceConfig
|
||||
if !b.Enabled {
|
||||
return sources
|
||||
}
|
||||
|
||||
sourceNames := make(map[string]string)
|
||||
|
||||
for _, source := range b.Sources {
|
||||
if !source.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := sourceNames[source.Name]; ok {
|
||||
logger.Warn(fmt.Sprintf("duplicate source name: %s", source.Name))
|
||||
continue
|
||||
}
|
||||
sourceNames[source.Name] = source.Name
|
||||
|
||||
sourceConfig, err := source.ToSourceConfig()
|
||||
if err != nil {
|
||||
logger.Warn(fmt.Sprintf("failed to convert source: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
sources = append(sources, sourceConfig)
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package blocklists
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/blocklist"
|
||||
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
|
||||
daemonBlocklist "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/blocklist/sources"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
)
|
||||
|
||||
type Sources struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Name string `mapstructure:"name"`
|
||||
URL string `mapstructure:"url"`
|
||||
Limit int `mapstructure:"limit"`
|
||||
Interval int64 `mapstructure:"interval"`
|
||||
Zip bool `mapstructure:"zip"`
|
||||
Format string `mapstructure:"format"`
|
||||
|
||||
JsonField string `mapstructure:"json_field"`
|
||||
|
||||
TxtType string `mapstructure:"txt_type"`
|
||||
TxtFieldIP int `mapstructure:"txt_field_ip"`
|
||||
TxtFieldIp2 int `mapstructure:"txt_field_ip2"`
|
||||
TxtFieldCIDR int `mapstructure:"txt_field_cidr"`
|
||||
TxtSeparator string `mapstructure:"txt_separator"`
|
||||
|
||||
RssTag string `mapstructure:"rss_tag"`
|
||||
RssField string `mapstructure:"rss_field"`
|
||||
RssFieldIP int `mapstructure:"rss_field_ip"`
|
||||
RssFieldSeparator string `mapstructure:"rss_field_separator"`
|
||||
}
|
||||
|
||||
func (s *Sources) ToSourceConfig() (*daemonBlocklist.SourceConfig, error) {
|
||||
if err := s.Validate(); err != nil {
|
||||
return &daemonBlocklist.SourceConfig{}, err
|
||||
}
|
||||
|
||||
pars, err := s.parser()
|
||||
if err != nil {
|
||||
return &daemonBlocklist.SourceConfig{}, err
|
||||
}
|
||||
|
||||
config := blocklist.NewConfig(uint(s.Limit))
|
||||
if s.TxtType == "interval" {
|
||||
config.Validator = &parser.IPRangeValidator{}
|
||||
}
|
||||
|
||||
if s.Zip {
|
||||
configZip := blocklist.NewConfigZip(config)
|
||||
return &daemonBlocklist.SourceConfig{
|
||||
Name: s.Name,
|
||||
Interval: time.Duration(s.Interval) * time.Second,
|
||||
Source: sources.NewBlocklistSourceZip(s.URL, pars, configZip),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &daemonBlocklist.SourceConfig{
|
||||
Name: s.Name,
|
||||
Interval: time.Duration(s.Interval) * time.Second,
|
||||
Source: sources.NewBlocklistSource(s.URL, pars, config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Sources) Validate() error {
|
||||
if err := validate.Name(s.Name, "sources.name"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Interval < 60 {
|
||||
return errors.New("invalid limit. Must be greater than or equal to 60 seconds")
|
||||
}
|
||||
|
||||
if s.Limit < 0 {
|
||||
return errors.New("invalid limit. Must be greater than or equal to 0")
|
||||
}
|
||||
|
||||
if s.URL == "" {
|
||||
return errors.New("url is required")
|
||||
} else if !strings.HasPrefix(s.URL, "http://") && !strings.HasPrefix(s.URL, "https://") {
|
||||
return errors.New("the URL must be to an HTTP or HTTPS resource")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Sources) parser() (parser.Parser, error) {
|
||||
switch s.Format {
|
||||
case "json":
|
||||
if s.JsonField == "" {
|
||||
return nil, errors.New("json_field is required")
|
||||
}
|
||||
return parserJson(s.JsonField)
|
||||
case "txt":
|
||||
return s.parserText()
|
||||
case "rss":
|
||||
if s.RssTag == "" {
|
||||
return nil, errors.New("rss_tag is required")
|
||||
}
|
||||
if s.RssField == "" {
|
||||
return nil, errors.New("rss_field is required")
|
||||
}
|
||||
if s.RssFieldIP < 0 {
|
||||
return nil, errors.New("rss_field_ip must be greater than or equal to 0")
|
||||
}
|
||||
return parserRss(s.RssTag, s.RssField, uint(s.RssFieldIP), s.RssFieldSeparator)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("format not support")
|
||||
}
|
||||
|
||||
func (s *Sources) parserText() (parser.Parser, error) {
|
||||
if s.TxtType == "" {
|
||||
return nil, errors.New("txt_type is required")
|
||||
}
|
||||
if s.TxtFieldIP < 0 {
|
||||
return nil, errors.New("txt_field_ip must be greater than or equal to 0")
|
||||
}
|
||||
if s.TxtSeparator == "" {
|
||||
return nil, errors.New("txt_separator is required")
|
||||
}
|
||||
switch s.TxtType {
|
||||
case "default":
|
||||
return parserTextDefault(uint8(s.TxtFieldIP), s.TxtSeparator)
|
||||
case "cidr":
|
||||
if s.TxtFieldCIDR < 0 {
|
||||
return nil, errors.New("txt_field_cidr must be greater than or equal to 0")
|
||||
}
|
||||
return parserTextCIDR(uint8(s.TxtFieldIP), uint8(s.TxtFieldCIDR), s.TxtSeparator)
|
||||
case "interval":
|
||||
if s.TxtFieldIp2 < 0 {
|
||||
return nil, errors.New("txt_field_ip2 must be greater than or equal to 0")
|
||||
}
|
||||
return parserTextInterval(uint8(s.TxtFieldIP), uint8(s.TxtFieldIp2), s.TxtSeparator)
|
||||
}
|
||||
return nil, fmt.Errorf("txt_type not support")
|
||||
}
|
||||
|
||||
func parserJson(fieldName string) (parser.Parser, error) {
|
||||
return parser.NewJsonLines(func(item json.RawMessage) (string, error) {
|
||||
var line map[string]any
|
||||
if err := json.Unmarshal(item, &line); err != nil {
|
||||
return "", fmt.Errorf("unmarshal json item: %w", err)
|
||||
}
|
||||
|
||||
v, ok := line[fieldName]
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
ip, ok := v.(string)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
})
|
||||
}
|
||||
|
||||
func parserTextDefault(ip uint8, separator string) (parser.Parser, error) {
|
||||
return parser.NewText(parser.NewDefaultTextExtract(ip, separator))
|
||||
}
|
||||
|
||||
func parserTextCIDR(ip uint8, cidr uint8, separator string) (parser.Parser, error) {
|
||||
textExtract := parser.NewCIDRTextExtract(ip, cidr, separator)
|
||||
return parser.NewText(textExtract)
|
||||
}
|
||||
|
||||
func parserTextInterval(ip uint8, ip2 uint8, separator string) (parser.Parser, error) {
|
||||
textExtract := parser.NewIntervalTextExtract(ip, ip2, separator)
|
||||
return parser.NewText(textExtract)
|
||||
}
|
||||
|
||||
func parserRss(itemTag string, fieldName string, fieldIP uint, separator string) (parser.Parser, error) {
|
||||
return parser.NewRss(func(decoder *xml.Decoder, start xml.StartElement) (string, error) {
|
||||
for {
|
||||
tok, err := decoder.Token()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch t := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if t.Name.Local != itemTag {
|
||||
continue
|
||||
}
|
||||
|
||||
value, err := parserRssReadFieldFromItem(decoder, t, fieldName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if value != "" {
|
||||
if separator != "" {
|
||||
fields := strings.Split(value, separator)
|
||||
if len(fields) <= int(fieldIP) {
|
||||
return "", nil
|
||||
}
|
||||
return strings.TrimSpace(fields[fieldIP]), nil
|
||||
}
|
||||
return strings.TrimSpace(value), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func parserRssReadFieldFromItem(decoder *xml.Decoder, start xml.StartElement, fieldTag string) (string, error) {
|
||||
depth := 1
|
||||
|
||||
for {
|
||||
tok, err := decoder.Token()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch t := tok.(type) {
|
||||
case xml.StartElement:
|
||||
depth++
|
||||
|
||||
if t.Name.Local == fieldTag {
|
||||
var value string
|
||||
if err := decoder.DecodeElement(&value, &t); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(value), nil
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
depth--
|
||||
if depth == 0 && t.Name.Local == start.Name.Local {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
"github.com/spf13/viper"
|
||||
@@ -14,6 +16,7 @@ type Setting struct {
|
||||
Options options
|
||||
MetadataNaming metadataNaming
|
||||
Policy policy
|
||||
PortKnocking []portKnocking
|
||||
}
|
||||
|
||||
func InitSetting(path string) (Setting, error) {
|
||||
@@ -49,6 +52,7 @@ func settingDefault() Setting {
|
||||
Options: defaultOptions(),
|
||||
MetadataNaming: defaultMetadataNaming(),
|
||||
Policy: defaultPolicy(),
|
||||
PortKnocking: defaultPortKnocking(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,3 +105,25 @@ func (s Setting) ToIPs() (IPs IPs, error error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s Setting) ToConfigPortKnocking() ([]firewall.ConfigPortKnocking, error) {
|
||||
fmt.Println(s.PortKnocking)
|
||||
|
||||
var configPortKnocking []firewall.ConfigPortKnocking
|
||||
|
||||
portKnockingNames := make(map[string]string)
|
||||
|
||||
for _, portKnocking := range s.PortKnocking {
|
||||
if _, ok := portKnockingNames[portKnocking.Name]; ok {
|
||||
return nil, fmt.Errorf("port knocking name %s is duplicated", portKnocking.Name)
|
||||
}
|
||||
portKnockingNames[portKnocking.Name] = portKnocking.Name
|
||||
|
||||
addPortKnocking, err := portKnocking.ToPortKnocking()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configPortKnocking = append(configPortKnocking, addPortKnocking)
|
||||
}
|
||||
return configPortKnocking, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
|
||||
"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/types"
|
||||
port2 "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"
|
||||
)
|
||||
@@ -99,7 +100,7 @@ func loopIP(baseConfigIP firewall.ConfigIP, directions []string, protocols []str
|
||||
if len(ports) == 0 {
|
||||
// If no port is specified, we only allow the IP address to be accepted.
|
||||
addIP.OnlyIP = true
|
||||
if addDirection == firewall.DirectionIn {
|
||||
if addDirection == types.DirectionIn {
|
||||
in = append(in, addIP)
|
||||
} else {
|
||||
out = append(out, addIP)
|
||||
@@ -108,13 +109,12 @@ func loopIP(baseConfigIP firewall.ConfigIP, directions []string, protocols []str
|
||||
}
|
||||
|
||||
if len(protocols) == 0 {
|
||||
addIP.Protocol = firewall.ProtocolTCP
|
||||
addIn, addOut, err := loopIPPort(addIP, ports, addDirection)
|
||||
addIn, addOut, err := loopIPPort(addIP, ports, addDirection, types.ProtocolTCP)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
if addDirection == firewall.DirectionIn {
|
||||
if addDirection == types.DirectionIn {
|
||||
in = append(in, addIn...)
|
||||
} else {
|
||||
out = append(out, addOut...)
|
||||
@@ -127,7 +127,7 @@ func loopIP(baseConfigIP firewall.ConfigIP, directions []string, protocols []str
|
||||
error = err
|
||||
return
|
||||
}
|
||||
if addDirection == firewall.DirectionIn {
|
||||
if addDirection == types.DirectionIn {
|
||||
in = append(in, addIn...)
|
||||
} else {
|
||||
out = append(out, addOut...)
|
||||
@@ -136,7 +136,7 @@ func loopIP(baseConfigIP firewall.ConfigIP, directions []string, protocols []str
|
||||
return
|
||||
}
|
||||
|
||||
func loopIPProtocol(baseConfigIP firewall.ConfigIP, protocols []string, ports []int, direction firewall.Direction) (in []firewall.ConfigIP, out []firewall.ConfigIP, error error) {
|
||||
func loopIPProtocol(baseConfigIP firewall.ConfigIP, protocols []string, ports []int, direction types.Direction) (in []firewall.ConfigIP, out []firewall.ConfigIP, error error) {
|
||||
for _, protocol := range protocols {
|
||||
addProtocol, err := port2.ToProtocol(protocol)
|
||||
if err != nil {
|
||||
@@ -144,10 +144,9 @@ func loopIPProtocol(baseConfigIP firewall.ConfigIP, protocols []string, ports []
|
||||
return
|
||||
}
|
||||
addIP := baseConfigIP
|
||||
addIP.Protocol = addProtocol
|
||||
|
||||
if len(ports) == 0 {
|
||||
if direction == firewall.DirectionIn {
|
||||
if direction == types.DirectionIn {
|
||||
in = append(in, addIP)
|
||||
} else {
|
||||
out = append(out, addIP)
|
||||
@@ -155,12 +154,12 @@ func loopIPProtocol(baseConfigIP firewall.ConfigIP, protocols []string, ports []
|
||||
continue
|
||||
}
|
||||
|
||||
addIn, addOut, err := loopIPPort(addIP, ports, direction)
|
||||
addIn, addOut, err := loopIPPort(addIP, ports, direction, addProtocol)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
if direction == firewall.DirectionIn {
|
||||
if direction == types.DirectionIn {
|
||||
in = append(in, addIn...)
|
||||
} else {
|
||||
out = append(out, addOut...)
|
||||
@@ -170,15 +169,22 @@ func loopIPProtocol(baseConfigIP firewall.ConfigIP, protocols []string, ports []
|
||||
return
|
||||
}
|
||||
|
||||
func loopIPPort(baseConfigIP firewall.ConfigIP, ports []int, direction firewall.Direction) (in []firewall.ConfigIP, out []firewall.ConfigIP, error error) {
|
||||
func loopIPPort(baseConfigIP firewall.ConfigIP, ports []int, direction types.Direction, protocol types.Protocol) (in []firewall.ConfigIP, out []firewall.ConfigIP, error error) {
|
||||
for _, port := range ports {
|
||||
if err := validate.Port(port, "port"); err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
|
||||
l4Port, err := types.NewL4Port(uint16(port), protocol)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
|
||||
addIP := baseConfigIP
|
||||
addIP.Port = uint16(port)
|
||||
if direction == firewall.DirectionIn {
|
||||
addIP.Port = l4Port
|
||||
if direction == types.DirectionIn {
|
||||
in = append(in, addIP)
|
||||
} else {
|
||||
out = append(out, addIP)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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/types"
|
||||
)
|
||||
|
||||
type policy struct {
|
||||
@@ -61,15 +62,15 @@ func (p policy) ToConfigPolicy() (firewall.ConfigPolicy, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p policy) dropToPolicyDrop(drop string, parametrName string) (firewall.PolicyDrop, error) {
|
||||
func (p policy) dropToPolicyDrop(drop string, parametrName string) (types.PolicyDrop, error) {
|
||||
if drop == "" {
|
||||
return 0, fmt.Errorf("%s is empty", parametrName)
|
||||
}
|
||||
switch drop {
|
||||
case "drop":
|
||||
return firewall.Drop, nil
|
||||
return types.Drop, nil
|
||||
case "reject":
|
||||
return firewall.Reject, nil
|
||||
return types.Reject, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid %s . Must be drop or reject", parametrName)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import (
|
||||
"errors"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
|
||||
port2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
|
||||
"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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
)
|
||||
|
||||
@@ -25,7 +26,7 @@ func (p *Port) ToPorts() (InPorts []firewall.ConfigPort, OutPorts []firewall.Con
|
||||
error = err
|
||||
return
|
||||
}
|
||||
action, err := port2.ToAction(p.Action)
|
||||
action, err := ip.ToAction(p.Action)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
@@ -37,25 +38,30 @@ func (p *Port) ToPorts() (InPorts []firewall.ConfigPort, OutPorts []firewall.Con
|
||||
return
|
||||
}
|
||||
for _, direction := range p.Directions {
|
||||
addDirection, err := port2.ToDirection(direction)
|
||||
addDirection, err := ip.ToDirection(direction)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
for _, protocol := range p.Protocols {
|
||||
addProtocol, err := port2.ToProtocol(protocol)
|
||||
addProtocol, err := ip.ToProtocol(protocol)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
|
||||
l4Port, err := types.NewL4Port(uint16(port), addProtocol)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
|
||||
addPort := firewall.ConfigPort{
|
||||
Number: uint16(port),
|
||||
Protocol: addProtocol,
|
||||
Port: l4Port,
|
||||
Action: action,
|
||||
LimitRate: p.LimitRate,
|
||||
}
|
||||
if addDirection == firewall.DirectionIn {
|
||||
if addDirection == types.DirectionIn {
|
||||
InPorts = append(InPorts, addPort)
|
||||
} else {
|
||||
OutPorts = append(OutPorts, addPort)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
port2 "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 portKnocking struct {
|
||||
Name string `mapstructure:"name"`
|
||||
IPVersion string `mapstructure:"ip_version"`
|
||||
Port int `mapstructure:"port"`
|
||||
Protocol string `mapstructure:"protocol"`
|
||||
Knocks []portKnockingKnock `mapstructure:"knock"`
|
||||
}
|
||||
|
||||
func defaultPortKnocking() []portKnocking {
|
||||
return []portKnocking{}
|
||||
}
|
||||
|
||||
func (p *portKnocking) ToPortKnocking() (firewall.ConfigPortKnocking, error) {
|
||||
if len(p.Knocks) == 0 {
|
||||
return firewall.ConfigPortKnocking{}, fmt.Errorf("port knocking must have at least one knock")
|
||||
}
|
||||
|
||||
if err := p.validate(); err != nil {
|
||||
return firewall.ConfigPortKnocking{}, err
|
||||
}
|
||||
|
||||
protocol, err := port2.ToProtocol(p.Protocol)
|
||||
if err != nil {
|
||||
return firewall.ConfigPortKnocking{}, err
|
||||
}
|
||||
|
||||
l4Port, err := types.NewL4Port(uint16(p.Port), protocol)
|
||||
if err != nil {
|
||||
return firewall.ConfigPortKnocking{}, err
|
||||
}
|
||||
|
||||
ipVersion, err := toVersionIP(p.IPVersion)
|
||||
if err != nil {
|
||||
return firewall.ConfigPortKnocking{}, err
|
||||
}
|
||||
|
||||
knocks := make([]*firewall.ConfigKnock, 0, len(p.Knocks))
|
||||
for _, knock := range p.Knocks {
|
||||
knock, err := knock.ToKnock()
|
||||
if err != nil {
|
||||
return firewall.ConfigPortKnocking{}, err
|
||||
}
|
||||
knocks = append(knocks, &knock)
|
||||
}
|
||||
|
||||
return firewall.ConfigPortKnocking{
|
||||
Name: p.Name,
|
||||
Port: l4Port,
|
||||
IPVersion: ipVersion,
|
||||
Knocks: knocks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *portKnocking) validate() error {
|
||||
if err := validate.Name(p.Name, "portKnocking.name"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validate.Port(p.Port, "portKnocking.port"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toVersionIP(versionIP string) (port2.Version, error) {
|
||||
switch strings.ToLower(versionIP) {
|
||||
case "ip4":
|
||||
return port2.IPv4, nil
|
||||
case "ip6":
|
||||
return port2.IPv6, nil
|
||||
default:
|
||||
return port2.IPv4, fmt.Errorf("invalid version_ip. Must be ip4 or ip6")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/types"
|
||||
port2 "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 portKnockingKnock struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Protocol string `mapstructure:"protocol"`
|
||||
Timeout int32 `mapstructure:"timeout"`
|
||||
Action string `mapstructure:"action"`
|
||||
}
|
||||
|
||||
func (k *portKnockingKnock) ToKnock() (firewall.ConfigKnock, error) {
|
||||
if err := k.validate(); err != nil {
|
||||
return firewall.ConfigKnock{}, err
|
||||
}
|
||||
|
||||
protocol, err := port2.ToProtocol(k.Protocol)
|
||||
if err != nil {
|
||||
return firewall.ConfigKnock{}, err
|
||||
}
|
||||
l4Port, err := types.NewL4Port(uint16(k.Port), protocol)
|
||||
if err != nil {
|
||||
return firewall.ConfigKnock{}, err
|
||||
}
|
||||
|
||||
action, err := port2.ToKnockAction(k.Action)
|
||||
if err != nil {
|
||||
return firewall.ConfigKnock{}, err
|
||||
}
|
||||
|
||||
return firewall.ConfigKnock{
|
||||
Port: l4Port,
|
||||
Action: action,
|
||||
Timeout: uint32(k.Timeout),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *portKnockingKnock) validate() error {
|
||||
if err := validate.Port(k.Port, "knock.port"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if k.Timeout <= 0 {
|
||||
return fmt.Errorf("knock.timeout must be positive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,14 +4,18 @@ import (
|
||||
"errors"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/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/notifications"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
logger "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
analyzerSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/analyzer"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/blocklists"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/docker"
|
||||
firewallSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/firewall"
|
||||
notificationsSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/notifications"
|
||||
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
@@ -20,6 +24,7 @@ type otherSettingsPath struct {
|
||||
Notifications string `mapstructure:"notifications"`
|
||||
Analyzer string `mapstructure:"analyzer"`
|
||||
Docker string `mapstructure:"docker"`
|
||||
Blocklists string `mapstructure:"blocklists"`
|
||||
}
|
||||
|
||||
func otherSettingsPathDefault() *otherSettingsPath {
|
||||
@@ -28,6 +33,7 @@ func otherSettingsPathDefault() *otherSettingsPath {
|
||||
Notifications: "/etc/kor-elf-shield/notifications.toml",
|
||||
Analyzer: "/etc/kor-elf-shield/analyzer.toml",
|
||||
Docker: "/etc/kor-elf-shield/docker.toml",
|
||||
Blocklists: "/etc/kor-elf-shield/blocklists.toml",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +63,11 @@ func (o *otherSettingsPath) ToFirewallConfig(dockerSupport bool) (firewall.Confi
|
||||
return firewall.Config{}, err
|
||||
}
|
||||
|
||||
portKnocking, err := setting.ToConfigPortKnocking()
|
||||
if err != nil {
|
||||
return firewall.Config{}, err
|
||||
}
|
||||
|
||||
return firewall.Config{
|
||||
InPorts: inPorts,
|
||||
OutPorts: outPorts,
|
||||
@@ -90,7 +101,8 @@ func (o *otherSettingsPath) ToFirewallConfig(dockerSupport bool) (firewall.Confi
|
||||
ChainOutputName: setting.MetadataNaming.ChainOutputName,
|
||||
ChainForwardName: setting.MetadataNaming.ChainForwardName,
|
||||
},
|
||||
Policy: configPolicy,
|
||||
Policy: configPolicy,
|
||||
PortKnocking: portKnocking,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -189,3 +201,18 @@ func (o *otherSettingsPath) ToDockerConfig(binaryLocations *binaryLocations) (co
|
||||
RuleStrategy: ruleStrategy,
|
||||
}, setting.Enabled, nil
|
||||
}
|
||||
|
||||
func (o *otherSettingsPath) ToBlocklistConfig(logger logger.Logger) (sources []*blocklist.SourceConfig, blocklistSupport bool, err error) {
|
||||
setting, err := blocklists.InitSetting(o.Blocklists)
|
||||
if err != nil {
|
||||
return []*blocklist.SourceConfig{}, false, err
|
||||
}
|
||||
|
||||
sources = setting.ToSources(logger)
|
||||
|
||||
if setting.Enabled && len(sources) == 0 {
|
||||
return []*blocklist.SourceConfig{}, false, errors.New(i18n.Lang.T("blocklist sources are empty"))
|
||||
}
|
||||
|
||||
return sources, setting.Enabled, nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package socket
|
||||
|
||||
import "net"
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/socket"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
Send(command string) (result string, err error)
|
||||
SendCommand(command string, args map[string]string) (result string, err error)
|
||||
Read() (string, error)
|
||||
Close() error
|
||||
}
|
||||
@@ -21,7 +27,35 @@ func NewSocketClient(path string) (Client, error) {
|
||||
}
|
||||
|
||||
func (s *client) Send(command string) (result string, err error) {
|
||||
_, err = s.conn.Write([]byte(command))
|
||||
msg := socket.Message{
|
||||
Command: command,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = s.conn.Write(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return s.Read()
|
||||
}
|
||||
|
||||
func (s *client) SendCommand(command string, args map[string]string) (string, error) {
|
||||
msg := socket.Message{
|
||||
Command: command,
|
||||
Args: args,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = s.conn.Write(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user