ebpf-blocker 1.0.0
XDP-based packet blocker using eBPF
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1/// \file main.cpp
2/// \brief Точка входа: event loop, обработка сигналов, метрики Prometheus.
3/// \details Содержит главный цикл приложения: загрузка конфигурации,
4/// инициализация БД и XDP, периодический опрос списков и
5/// обслуживание HTTP-эндпоинта /metrics.
6
7#include "main.h"
8#include "db.h"
9#include "xdp_loader.h"
10
11#include "products_log/products_log.h"
12#include <cstdlib>
13
14#include <arpa/inet.h>
15#include <csignal>
16#include <cstring>
17#include <ctime>
18#include <fstream>
19#include <getopt.h>
20#include <iostream>
21#include <map>
22#include <shared_mutex>
23#include <sstream>
24#include <thread>
25#include <unistd.h>
26
27#include "httplib.h"
28
29/**********************************************************************/
30/* Global state */
31/**********************************************************************/
32
33std::atomic<bool> g_running{true};
34std::atomic<bool> g_xdp_attached{false};
35std::atomic<bool> g_detach_requested{false};
36
37/// Потокобезопасный аккумулятор метрик Prometheus.
39 std::map<uint32_t, uint64_t>
40 total_drops; ///< Суммарное количество дропов с запуска процесса.
41 std::map<uint32_t, uint64_t>
42 interval_drops; ///< Дропов за последний интервал опроса.
43 std::map<uint32_t, uint64_t> hourly_drops; ///< Дропов с начала текущего часа.
44 std::map<uint32_t, uint64_t> daily_drops; ///< Дропов с начала текущего дня.
45 size_t whitelist_size = 0; ///< Текущее количество записей в whitelist.
46 size_t blacklist_size = 0; ///< Текущее количество записей в blacklist.
47 bool xdp_attached = false; ///< Прикреплён ли XDP в данный момент.
48 std::shared_mutex mtx; ///< Защищает все поля выше.
49};
50
51static XdpBlocker *g_blocker = nullptr;
52
53/**********************************************************************/
54/* Signal handler */
55/**********************************************************************/
56
57/// Обрабатывает сигналы ОС для управления жизненным циклом.
58///
59/// - SIGINT / SIGTERM: запрос плавного завершения.
60/// - SIGUSR1: запрос открепления XDP (аварийное отключение).
61/// @param[in] sig Номер сигнала.
62static void HandleSignal(int sig) {
63 switch (sig) {
64 case SIGINT:
65 case SIGTERM:
66 g_running = false;
67 break;
68 case SIGUSR1:
69 g_detach_requested = true;
70 break;
71 }
72}
73
74/**********************************************************************/
75/* Argument parsing */
76/**********************************************************************/
77
78/// Парсит аргументы командной строки через getopt_long.
79/// @param[in] argc Количество аргументов из main().
80/// @param[in] argv Массив аргументов из main().
81/// @return Заполненная структура Config (для пропущенных флагов —
82/// значения по умолчанию).
83static Config ParseArgs(int argc, char *argv[]) {
84 Config cfg;
85 static struct option long_opts[] = {
86 {"iface", required_argument, nullptr, 'i'},
87 {"db-path", required_argument, nullptr, 'd'},
88 {"whitelist", required_argument, nullptr, 'w'},
89 {"interval", required_argument, nullptr, 't'},
90 {"metrics-listen", required_argument, nullptr, 'm'},
91 {"help", no_argument, nullptr, 'h'},
92 {nullptr, 0, nullptr, 0}};
93
94 int opt;
95 while ((opt = getopt_long(argc, argv, "i:d:w:t:m:h", long_opts, nullptr)) !=
96 -1) {
97 switch (opt) {
98 case 'i':
99 cfg.iface = optarg;
100 break;
101 case 'd':
102 cfg.db_path = optarg;
103 break;
104 case 'w':
105 cfg.whitelist_conf = optarg;
106 break;
107 case 't':
108 cfg.interval_sec = std::stoi(optarg);
109 break;
110 case 'm':
111 cfg.metrics_listen = optarg;
112 break;
113 case 'h':
114 std::cout
115 << "Usage: ebpf-blocker [options]\n"
116 << " -i, --iface IFACE Network interface (default: eth0)\n"
117 << " -d, --db-path PATH SQLite database path\n"
118 << " -w, --whitelist PATH Whitelist config file\n"
119 << " -t, --interval SEC Poll interval (default: 60)\n"
120 << " -m, --metrics-listen ADDR Metrics HTTP listen address "
121 "(default: 0.0.0.0:9101)\n"
122 << " -h, --help Show this help\n";
123 exit(0);
124 }
125 }
126 return cfg;
127}
128
129/**********************************************************************/
130/* Whitelist file parser */
131/**********************************************************************/
132
133/// Читает записи белого списка из конфигурационного файла.
134///
135/// Формат: одна IP/CIDR на строку, опционально '# комментарий' в конце.
136/// Пустые строки и строки, начинающиеся с '#', игнорируются.
137/// @param[in] path Путь к файлу конфигурации белого списка.
138/// @return Список распарсенных записей.
139static std::vector<BlockEntry> ReadWhitelistFile(const std::string &path) {
140 std::vector<BlockEntry> entries;
141 std::ifstream file(path);
142 if (!file.is_open())
143 return entries;
144
145 std::string line;
146 while (std::getline(file, line)) {
147 if (line.empty() || line[0] == '#')
148 continue;
149 auto comment_pos = line.find('#');
150 std::string ip =
151 (comment_pos != std::string::npos) ? line.substr(0, comment_pos) : line;
152 ip.erase(ip.find_last_not_of(" \t\r\n") + 1);
153 ip.erase(0, ip.find_first_not_of(" \t\r\n"));
154 if (ip.empty())
155 continue;
156 std::string comment = "whitelist.conf";
157 if (comment_pos != std::string::npos) {
158 comment = line.substr(comment_pos + 1);
159 comment.erase(0, comment.find_first_not_of(" \t\r\n"));
160 }
161 entries.push_back({ip, comment});
162 }
163 return entries;
164}
165
166#include <algorithm>
167
168/**********************************************************************/
169/* Logging helpers */
170/**********************************************************************/
171
172/// Логирует текущее количество записей в whitelist и blacklist (heartbeat).
173/// @param[in] whitelist Список записей белого списка.
174/// @param[in] blacklist Список записей чёрного списка.
175static void PrintStats(const std::vector<BlockEntry> &whitelist,
176 const std::vector<BlockEntry> &blacklist) {
177 std::cout << "whitelist: " << whitelist.size() << " entries, "
178 << "blacklist: " << blacklist.size() << " entries" << std::endl;
179}
180
181/// Удобный алиас для карты соответствия IP (uint32) количеству дропов.
182using DropMap = std::map<uint32_t, uint64_t>;
183
184/// Логирует статистику дропов за последний интервал опроса (всего + топ).
185/// @param[in] drops Вектор пар (source_ip, количество_дропов).
186static void
187PrintIntervalDrops(const std::vector<std::pair<uint32_t, uint64_t>> &drops) {
188 uint64_t total = 0;
189 for (const auto &d : drops)
190 total += d.second;
191
192 std::cout << "drops: " << total << " packets";
193
194 if (!drops.empty()) {
195 auto top = std::max_element(
196 drops.begin(), drops.end(),
197 [](const auto &a, const auto &b) { return a.second < b.second; });
198
199 char buf[INET_ADDRSTRLEN];
200 inet_ntop(AF_INET, &top->first, buf, sizeof(buf));
201 std::cout << " | top: " << buf << " (" << top->second << ")";
202 }
203
204 std::cout << std::endl;
205}
206
207/// Логирует периодическую статистику дропов (почасовую/ежедневную) с топ-5
208/// нарушителями.
209/// @param[in] label Метка периода ("почасовая" или "ежедневная").
210/// @param[in] drops Карта дропов (IP -> количество).
211/// @param[in] timestamp Временная метка для лога.
212static void PrintTopDrops(const std::string &label, const DropMap &drops,
213 const char *timestamp) {
214 uint64_t total = 0;
215 for (const auto &d : drops)
216 total += d.second;
217
218 std::cout << "[" << timestamp << "] === " << label << " ===" << std::endl;
219 std::cout << " drops: " << total << " packets" << std::endl;
220
221 if (!drops.empty()) {
222 using Pair = std::pair<uint32_t, uint64_t>;
223 std::vector<Pair> sorted(drops.begin(), drops.end());
224 std::sort(sorted.begin(), sorted.end(),
225 [](const Pair &a, const Pair &b) { return a.second > b.second; });
226
227 std::cout << " top offenders:" << std::endl;
228 int count = 0;
229 char buf[INET_ADDRSTRLEN];
230 for (const auto &d : sorted) {
231 if (count++ >= 5)
232 break;
233 inet_ntop(AF_INET, &d.first, buf, sizeof(buf));
234 std::cout << " " << buf << " " << d.second << std::endl;
235 }
236 }
237 std::cout << std::endl;
238}
239
240/**********************************************************************/
241/* Main entry point */
242/**********************************************************************/
243
244/// Точка входа.
245///
246/// Инициализирует БД, загружает XDP-программу, запускает event loop
247/// с периодическим опросом списков и HTTP-сервером метрик Prometheus.
248/// @return 0 при успешном завершении.
249int main(int argc, char *argv[]) {
250 Config cfg = ParseArgs(argc, argv);
251
252 signal(SIGINT, HandleSignal);
253 signal(SIGTERM, HandleSignal);
254 signal(SIGUSR1, HandleSignal);
255
256 try {
257 BlockerDB db(cfg.db_path);
258 db.Migrate();
259
260 XdpBlocker blocker;
261 g_blocker = &blocker;
262
263 blocker.Load(cfg.iface);
264 g_xdp_attached = true;
265
266 auto whitelist_conf = ReadWhitelistFile(cfg.whitelist_conf);
267 if (!whitelist_conf.empty()) {
268 std::cout << "loaded " << whitelist_conf.size() << " entries from "
269 << cfg.whitelist_conf << std::endl;
270 }
271
272 auto whitelist_db = db.GetWhitelist();
273 whitelist_conf.insert(whitelist_conf.end(), whitelist_db.begin(),
274 whitelist_db.end());
275
276 auto blacklist = db.GetBlacklist();
277
278 if (whitelist_conf.empty()) {
279 std::cerr << "WARNING: whitelist is empty! "
280 << "Your SSH connection may be at risk. "
281 << "Sleeping 30 seconds before applying rules..." << std::endl;
282 for (int i = 0; i < 30 && g_running; i++)
283 sleep(1);
284 }
285
286 blocker.UpdateWhitelist(whitelist_conf);
287 blocker.UpdateBlacklist(blacklist);
288
289 std::cout << "blocker started on " << cfg.iface << " (poll every "
290 << cfg.interval_sec << "s)" << std::endl;
291 PrintStats(whitelist_conf, blacklist);
292
293 // Регистрация запуска в Products API. Версия приходит из PRODUCTS_VERSION
294 // (генерируется CI из количества коммитов); без env-файла — "dev".
295 const char *env_version = std::getenv("PRODUCTS_VERSION");
296 std::string products_version =
297 (env_version != nullptr && *env_version != '\0') ? env_version : "dev";
298 auto products_log = std::make_unique<products_log::ProductsLog>(
299 "ebpf-blocker", products_version);
300 products_log->PostStart();
301
302 auto metrics = std::make_shared<MetricsStore>();
303 {
304 std::lock_guard<std::shared_mutex> lock(metrics->mtx);
305 metrics->whitelist_size = whitelist_conf.size();
306 metrics->blacklist_size = blacklist.size();
307 metrics->xdp_attached = true;
308 metrics->total_drops.clear();
309 }
310
311 std::thread http_thread([metrics, &cfg]() {
312 httplib::Server svr;
313 svr.Get("/metrics", [metrics](const httplib::Request &,
314 httplib::Response &res) {
315 std::shared_lock<std::shared_mutex> lock(metrics->mtx);
316 std::ostringstream ss;
317 ss << "# HELP ebpf_blocker_whitelist_entries Current whitelist "
318 "entries\n"
319 << "# TYPE ebpf_blocker_whitelist_entries gauge\n"
320 << "ebpf_blocker_whitelist_entries " << metrics->whitelist_size
321 << "\n"
322 << "# HELP ebpf_blocker_blacklist_entries Current blacklist "
323 "entries\n"
324 << "# TYPE ebpf_blocker_blacklist_entries gauge\n"
325 << "ebpf_blocker_blacklist_entries " << metrics->blacklist_size
326 << "\n"
327 << "# HELP ebpf_blocker_xdp_attached XDP attached flag (1=attached, "
328 "0=detached)\n"
329 << "# TYPE ebpf_blocker_xdp_attached gauge\n"
330 << "ebpf_blocker_xdp_attached " << (metrics->xdp_attached ? 1 : 0)
331 << "\n"
332 << "# HELP ebpf_blocker_drops_total Total dropped packets since "
333 "process start\n"
334 << "# TYPE ebpf_blocker_drops_total counter\n";
335 char ip_buf[INET_ADDRSTRLEN];
336 for (const auto &[ip, count] : metrics->total_drops) {
337 inet_ntop(AF_INET, &ip, ip_buf, sizeof(ip_buf));
338 ss << "ebpf_blocker_drops_total{ip=\"" << ip_buf << "\"} " << count
339 << "\n";
340 }
341 ss << "# HELP ebpf_blocker_drops_interval Drops in last poll interval\n"
342 << "# TYPE ebpf_blocker_drops_interval gauge\n";
343 if (metrics->interval_drops.empty()) {
344 ss << "ebpf_blocker_drops_interval 0\n";
345 } else {
346 for (const auto &[ip, count] : metrics->interval_drops) {
347 inet_ntop(AF_INET, &ip, ip_buf, sizeof(ip_buf));
348 ss << "ebpf_blocker_drops_interval{ip=\"" << ip_buf << "\"} "
349 << count << "\n";
350 }
351 }
352 ss << "# HELP ebpf_blocker_drops_hourly Drops since start of current "
353 "hour\n"
354 << "# TYPE ebpf_blocker_drops_hourly gauge\n";
355 if (metrics->hourly_drops.empty()) {
356 ss << "ebpf_blocker_drops_hourly 0\n";
357 } else {
358 for (const auto &[ip, count] : metrics->hourly_drops) {
359 inet_ntop(AF_INET, &ip, ip_buf, sizeof(ip_buf));
360 ss << "ebpf_blocker_drops_hourly{ip=\"" << ip_buf << "\"} " << count
361 << "\n";
362 }
363 }
364 ss << "# HELP ebpf_blocker_drops_daily Drops since start of current "
365 "day\n"
366 << "# TYPE ebpf_blocker_drops_daily gauge\n";
367 if (metrics->daily_drops.empty()) {
368 ss << "ebpf_blocker_drops_daily 0\n";
369 } else {
370 for (const auto &[ip, count] : metrics->daily_drops) {
371 inet_ntop(AF_INET, &ip, ip_buf, sizeof(ip_buf));
372 ss << "ebpf_blocker_drops_daily{ip=\"" << ip_buf << "\"} " << count
373 << "\n";
374 }
375 }
376 res.set_content(ss.str(), "text/plain; charset=utf-8");
377 });
378 std::cout << "metrics HTTP server listening on " << cfg.metrics_listen
379 << std::endl;
380 auto colon = cfg.metrics_listen.find_last_of(':');
381 auto host = cfg.metrics_listen.substr(0, colon);
382 auto port = std::stoi(cfg.metrics_listen.substr(colon + 1));
383 if (!svr.listen(host, port))
384 std::cerr << "failed to start metrics HTTP server on "
385 << cfg.metrics_listen << std::endl;
386 });
387 http_thread.detach();
388
389 int prev_hour = -1;
390 int prev_day = -1;
391
392 while (g_running) {
393 for (int i = 0; i < cfg.interval_sec && g_running; i++)
394 sleep(1);
395
396 if (!g_running)
397 break;
398
399 if (g_detach_requested) {
400 g_blocker->Unload();
401 g_xdp_attached = false;
402 g_detach_requested = false;
403 std::cout << "SIGUSR1: XDP detached, traffic passes through"
404 << std::endl;
405 }
406
407 time_t now = time(nullptr);
408 struct tm tm_now;
409 localtime_r(&now, &tm_now);
410
411 int cur_hour = tm_now.tm_hour;
412 int cur_min = tm_now.tm_min;
413 int cur_day = tm_now.tm_yday;
414
415 if (!g_xdp_attached) {
416 try {
417 blocker.Load(cfg.iface);
418 g_xdp_attached = true;
419 std::cout << "XDP re-attached" << std::endl;
420 } catch (const std::exception &e) {
421 std::cerr << "failed to re-attach XDP: " << e.what() << std::endl;
422 continue;
423 }
424 }
425
426 try {
427 whitelist_conf = ReadWhitelistFile(cfg.whitelist_conf);
428 whitelist_db = db.GetWhitelist();
429 whitelist_conf.insert(whitelist_conf.end(), whitelist_db.begin(),
430 whitelist_db.end());
431
432 blacklist = db.GetBlacklist();
433
434 blocker.UpdateWhitelist(whitelist_conf);
435 blocker.UpdateBlacklist(blacklist);
436
437 auto drops = blocker.ReadAndClearDropCounters();
438
439 for (const auto &d : drops)
440 db.UpdateLastBlockedAt(d.first);
441
442 PrintStats(whitelist_conf, blacklist);
443 PrintIntervalDrops(drops);
444
445 {
446 std::lock_guard<std::shared_mutex> lock(metrics->mtx);
447 for (const auto &d : drops) {
448 metrics->total_drops[d.first] += d.second;
449 metrics->hourly_drops[d.first] += d.second;
450 metrics->daily_drops[d.first] += d.second;
451 }
452 metrics->interval_drops.clear();
453 for (const auto &d : drops)
454 metrics->interval_drops[d.first] += d.second;
455 metrics->whitelist_size = whitelist_conf.size();
456 metrics->blacklist_size = blacklist.size();
457 metrics->xdp_attached = g_xdp_attached;
458 }
459
460 char ts[64];
461 strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M", &tm_now);
462
463 if (cur_min == 0 && cur_hour != prev_hour) {
464 DropMap snapshot;
465 {
466 std::lock_guard<std::shared_mutex> lock(metrics->mtx);
467 snapshot = metrics->hourly_drops;
468 metrics->hourly_drops.clear();
469 }
470 if (!snapshot.empty())
471 PrintTopDrops("Hourly stats (last 60 min)", snapshot, ts);
472 prev_hour = cur_hour;
473 }
474
475 if (cur_hour == 0 && cur_min == 0 && cur_day != prev_day) {
476 DropMap snapshot;
477 {
478 std::lock_guard<std::shared_mutex> lock(metrics->mtx);
479 snapshot = metrics->daily_drops;
480 metrics->daily_drops.clear();
481 }
482 if (!snapshot.empty())
483 PrintTopDrops("Daily stats (last 24h)", snapshot, ts);
484 prev_day = cur_day;
485 }
486 } catch (const std::exception &e) {
487 std::cerr << "update error: " << e.what() << std::endl;
488 }
489 }
490
491 std::cout << "shutting down..." << std::endl;
492 products_log->PostShutdown();
493 blocker.Unload();
494 g_blocker = nullptr;
495
496 } catch (const std::exception &e) {
497 std::cerr << "fatal error: " << e.what() << std::endl;
498 return 1;
499 }
500
501 return 0;
502}
Definition db.h:41
std::vector< BlockEntry > GetBlacklist()
Definition db.cpp:198
void Migrate()
Definition db.cpp:89
void UpdateLastBlockedAt(uint32_t ip)
Definition db.cpp:146
std::vector< BlockEntry > GetWhitelist()
Definition db.cpp:194
std::vector< std::pair< uint32_t, uint64_t > > ReadAndClearDropCounters()
Читает все записи из карты drop_counter и атомарно очищает их.
void Unload()
Открепляет XDP и закрывает BPF-объект. Безопасен для многократного вызова.
void UpdateWhitelist(const std::vector< BlockEntry > &entries)
void UpdateBlacklist(const std::vector< BlockEntry > &entries)
void Load(const std::string &iface)
Обёртка SQLite-базы данных и утилиты парсинга CIDR.
std::atomic< bool > g_xdp_attached
Глобальный флаг: true когда XDP-программа прикреплена к интерфейсу.
Definition main.cpp:34
int main(int argc, char *argv[])
Definition main.cpp:249
static std::vector< BlockEntry > ReadWhitelistFile(const std::string &path)
Definition main.cpp:139
static XdpBlocker * g_blocker
Definition main.cpp:51
static void PrintIntervalDrops(const std::vector< std::pair< uint32_t, uint64_t > > &drops)
Definition main.cpp:187
std::atomic< bool > g_detach_requested
Глобальный флаг: true когда SIGUSR1 запросил detach XDP.
Definition main.cpp:35
static void PrintTopDrops(const std::string &label, const DropMap &drops, const char *timestamp)
Definition main.cpp:212
static void PrintStats(const std::vector< BlockEntry > &whitelist, const std::vector< BlockEntry > &blacklist)
Definition main.cpp:175
std::map< uint32_t, uint64_t > DropMap
Удобный алиас для карты соответствия IP (uint32) количеству дропов.
Definition main.cpp:182
static Config ParseArgs(int argc, char *argv[])
Definition main.cpp:83
std::atomic< bool > g_running
Глобальный флаг: false запускает graceful shutdown.
Definition main.cpp:33
static void HandleSignal(int sig)
Definition main.cpp:62
Конфигурация и глобальное состояние eBPF Blocker.
Конфигурация приложения, полученная из аргументов командной строки.
Definition main.h:11
std::string metrics_listen
Адрес HTTP-сервера метрик.
Definition main.h:17
std::string iface
Сетевой интерфейс для прикрепления XDP.
Definition main.h:12
std::string whitelist_conf
Файл конфигурации белого списка.
Definition main.h:15
int interval_sec
Интервал опроса БД в секундах.
Definition main.h:18
std::string db_path
Путь к SQLite-базе данных.
Definition main.h:13
Потокобезопасный аккумулятор метрик Prometheus.
Definition main.cpp:38
std::map< uint32_t, uint64_t > daily_drops
Дропов с начала текущего дня.
Definition main.cpp:44
std::shared_mutex mtx
Защищает все поля выше.
Definition main.cpp:48
std::map< uint32_t, uint64_t > total_drops
Суммарное количество дропов с запуска процесса.
Definition main.cpp:40
std::map< uint32_t, uint64_t > hourly_drops
Дропов с начала текущего часа.
Definition main.cpp:43
bool xdp_attached
Прикреплён ли XDP в данный момент.
Definition main.cpp:47
std::map< uint32_t, uint64_t > interval_drops
Дропов за последний интервал опроса.
Definition main.cpp:42
size_t blacklist_size
Текущее количество записей в blacklist.
Definition main.cpp:46
size_t whitelist_size
Текущее количество записей в whitelist.
Definition main.cpp:45
Загрузчик BPF-объекта и менеджер XDP-программы.