dhcp-log.pl 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. #!/usr/bin/perl
  2. #
  3. # Автор: Roman Dmitiriev <rnd@rajven.ru>
  4. # Назначение: Скрипт для обработки DHCP-логов, определения подключений клиентов
  5. # через коммутаторы по данным из DHCP Option 82 (remote-id / circuit-id)
  6. # и записи соединений в базу данных.
  7. #
  8. use utf8;
  9. use open ":encoding(utf8)";
  10. use Encode;
  11. no warnings 'utf8';
  12. use English;
  13. use base;
  14. use FindBin '$Bin';
  15. use lib "/opt/Eye/scripts";
  16. use Data::Dumper;
  17. use eyelib::config;
  18. use eyelib::main;
  19. use eyelib::database;
  20. use eyelib::net_utils;
  21. use strict;
  22. use warnings;
  23. use Getopt::Long;
  24. use Proc::Daemon;
  25. use POSIX;
  26. use Net::Netmask;
  27. use Text::Iconv;
  28. use File::Tail;
  29. use Fcntl qw(:flock);
  30. # === БЛОКИРОВКА И ИНИЦИАЛИЗАЦИЯ ===
  31. # Блокировка запуска нескольких экземпляров скрипта
  32. open(SELF, "<", $0) or die "Cannot open $0 - $!";
  33. flock(SELF, LOCK_EX | LOCK_NB) or exit 1;
  34. # Установка низкого приоритета процесса (nice = 19)
  35. setpriority(0, 0, 19);
  36. # === ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ===
  37. my $mute_time = 300; # Время (в секундах) для подавления дублирующих DHCP-событий
  38. my $log_file = '/var/log/dhcp.log';
  39. # Определяем имя процесса и PID-файл
  40. my $proc_name = $MY_NAME;
  41. $proc_name =~ s/\.[^.]+$//;
  42. my $pid_file = '/run/eye/' . $proc_name;
  43. my $pf = $pid_file . '.pid';
  44. # Настройка демона
  45. my $daemon = Proc::Daemon->new(
  46. pid_file => $pf,
  47. work_dir => $HOME_DIR
  48. );
  49. # Проверяем, запущен ли уже процесс
  50. my $pid = $daemon->Status($pf);
  51. my $daemonize = 1; # По умолчанию — запуск в фоне
  52. # === ОБРАБОТКА АРГУМЕНТОВ КОМАНДНОЙ СТРОКИ ===
  53. GetOptions(
  54. 'daemon!' => \$daemonize,
  55. "help" => \&usage,
  56. "reload" => \&reload,
  57. "restart" => \&restart,
  58. "start" => \&run,
  59. "status" => \&status,
  60. "stop" => \&stop # опечатка в оригинале — исправлено
  61. ) or &usage;
  62. exit(0);
  63. # === ФУНКЦИИ УПРАВЛЕНИЯ ДЕМОНОМ ===
  64. sub stop {
  65. log_info("Запрошена остановка демона...");
  66. if ($pid) {
  67. print "Stopping pid $pid...";
  68. if ($daemon->Kill_Daemon($pf)) {
  69. print "Successfully stopped.\n";
  70. log_info("Демон успешно остановлен (PID $pid).");
  71. } else {
  72. print "Could not find $pid. Was it running?\n";
  73. log_warning("Не удалось остановить процесс PID $pid — возможно, он уже завершён.");
  74. }
  75. } else {
  76. print "Not running, nothing to stop.\n";
  77. log_info("Демон не запущен — останавливать нечего.");
  78. }
  79. }
  80. sub status {
  81. if ($pid) {
  82. print "Running with pid $pid.\n";
  83. log_info("Статус: демон запущен (PID $pid).");
  84. } else {
  85. print "Not running.\n";
  86. log_info("Статус: демон не запущен.");
  87. }
  88. }
  89. sub run {
  90. log_info("Запуск основного цикла обработки DHCP-логов...");
  91. if ($pid) {
  92. print "Already Running with pid $pid\n";
  93. log_warning("Попытка запуска уже работающего демона (PID $pid).");
  94. return;
  95. }
  96. print "Starting...\n";
  97. log_info("Инициализация демона...");
  98. if ($daemonize) {
  99. # Инициализация демона: закрытие дескрипторов, смена директории и т.п.
  100. $daemon->Init;
  101. log_debug("Демон инициализирован в фоновом режиме.");
  102. }
  103. setpriority(0, 0, 19); # Убедимся, что приоритет установлен и в дочернем процессе
  104. # Конвертер для перекодирования из cp866 в UTF-8 (для старых логов)
  105. my $converter = Text::Iconv->new("cp866", "utf8");
  106. # Основной бесконечный цикл обработки логов
  107. while (1) {
  108. eval {
  109. log_debug("Начало нового цикла обработки DHCP-логов.");
  110. my %leases; # кэш для подавления дублей
  111. # Создаём новое подключение к БД
  112. my $hdb = init_db();
  113. log_debug("Подключение к БД установлено.");
  114. # Открываем лог-файл для "хвостового" чтения (tail -f)
  115. my $dhcp_log = File::Tail->new(
  116. name => $log_file,
  117. maxinterval => 5,
  118. interval => 1,
  119. ignore_nonexistent => 1
  120. ) || die "$log_file not found!";
  121. log_info("Начинаю чтение логов из $log_file...");
  122. while (my $logline = $dhcp_log->read) {
  123. next unless $logline;
  124. chomp($logline);
  125. log_verbose("Получена строка из лога: $logline");
  126. # Удаляем непечатаемые символы (кроме букв, цифр, пунктуации и пробелов)
  127. $logline =~ s/[^\p{L}\p{N}\p{P}\p{Z}]//g;
  128. log_debug("Строка после фильтрации: $logline");
  129. # Разбираем строку по точке с запятой
  130. my (
  131. $type, $mac, $ip, $hostname, $timestamp,
  132. $tags, $sup_hostname, $old_hostname,
  133. $circuit_id, $remote_id, $client_id,
  134. $decoded_circuit_id, $decoded_remote_id
  135. ) = split(/;/, $logline);
  136. # Пропускаем строки без типа или не относящиеся к DHCP-событиям
  137. next unless $type && $type =~ /^(old|add|del)$/i;
  138. log_debug("Обрабатываем DHCP-событие: тип='$type', MAC='$mac', IP='$ip'");
  139. # Подавление дублей с одинаковым IP и типом в течение $mute_time секунд
  140. if (exists $leases{$ip} && $leases{$ip}{type} eq $type && (time() - $leases{$ip}{last_time} <= $mute_time)) {
  141. log_debug("Пропускаем дубликат: IP=$ip, тип=$type (в пределах $mute_time сек)");
  142. next;
  143. }
  144. # Обновляем конфиг каждые 60 секунд
  145. if (time() - $last_refresh_config >= 60) {
  146. log_debug("Обновление конфигурации...");
  147. init_option($hdb);
  148. }
  149. # Обрабатываем DHCP-запрос: обновление/создание записи в базе
  150. my $dhcp_record = process_dhcp_request($hdb, $type, $mac, $ip, $hostname, $client_id, $decoded_circuit_id, $decoded_remote_id);
  151. next unless $dhcp_record;
  152. # Сохраняем в кэш для подавления дублей
  153. $leases{$ip} = {
  154. type => $type,
  155. last_time => time()
  156. };
  157. my $auth_id = $dhcp_record->{auth_id};
  158. # === ЛОГИКА ОПРЕДЕЛЕНИЯ КОММУТАТОРА И ПОРТА ===
  159. my ($switch, $switch_port);
  160. my ($t_remote_id, $t_circuit_id) = ($remote_id, $circuit_id);
  161. # Обрабатываем только события подключения (add/old)
  162. if ($type =~ /^(add|old)$/i) {
  163. log_debug("Пытаемся определить коммутатор по данным Option 82...");
  164. # 1. Пытаемся определить по декодированному remote-id как MAC
  165. if ($decoded_remote_id) {
  166. $t_remote_id = $decoded_remote_id;
  167. $t_remote_id .= "0" x (12 - length($t_remote_id)) if length($t_remote_id) < 12;
  168. $t_remote_id = mac_splitted(isc_mac_simplify($t_remote_id));
  169. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  170. "FROM `devices` AS D, `User_auth` AS A " .
  171. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  172. "AND A.mac = '$t_remote_id'";
  173. log_debug("SQL (по decoded_remote_id): $devSQL");
  174. $switch = get_record_sql($hdb, $devSQL);
  175. if ($switch) {
  176. $remote_id = $t_remote_id;
  177. $circuit_id = $decoded_circuit_id;
  178. $dhcp_record->{'circuit-id'} = $circuit_id;
  179. $dhcp_record->{'remote-id'} = $remote_id;
  180. log_debug("Коммутатор найден по decoded_remote_id: " . $switch->{device_name});
  181. }
  182. }
  183. # 2. Если не нашли — пробуем по оригинальному remote-id
  184. if (!$switch && $remote_id) {
  185. $t_remote_id = $remote_id;
  186. $t_remote_id .= "0" x (12 - length($t_remote_id)) if length($t_remote_id) < 12;
  187. $t_remote_id = mac_splitted(isc_mac_simplify($t_remote_id));
  188. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  189. "FROM `devices` AS D, `User_auth` AS A " .
  190. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  191. "AND A.mac = '$t_remote_id'";
  192. log_debug("SQL (по remote_id): $devSQL");
  193. $switch = get_record_sql($hdb, $devSQL);
  194. if ($switch) {
  195. $remote_id = $t_remote_id;
  196. $dhcp_record->{'circuit-id'} = $circuit_id;
  197. $dhcp_record->{'remote-id'} = $remote_id;
  198. log_debug("Коммутатор найден по remote_id: " . $switch->{device_name});
  199. }
  200. }
  201. # 3. Если не нашли — пробуем по имени устройства (remote_id как строка)
  202. if (!$switch && $remote_id) {
  203. my @id_words = split(/ /, $remote_id);
  204. if ($id_words[0]) {
  205. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  206. "FROM `devices` AS D, `User_auth` AS A " .
  207. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  208. "AND D.device_name LIKE '$id_words[0]%'";
  209. log_debug("SQL (по имени устройства из remote_id): $devSQL");
  210. $switch = get_record_sql($hdb, $devSQL);
  211. if ($switch) {
  212. log_debug("Коммутатор найден по имени: " . $switch->{device_name});
  213. }
  214. }
  215. }
  216. # 4. Специальный случай: MikroTik (circuit-id может содержать имя)
  217. if (!$switch && $circuit_id) {
  218. my @id_words = split(/ /, $circuit_id);
  219. if ($id_words[0]) {
  220. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  221. "FROM `devices` AS D, `User_auth` AS A " .
  222. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  223. "AND D.device_name LIKE '$id_words[0]%'";
  224. log_debug("SQL (по имени из circuit_id — MikroTik?): $devSQL");
  225. $switch = get_record_sql($hdb, $devSQL);
  226. if ($switch) {
  227. # MikroTik часто путает remote-id и circuit-id — меняем местами
  228. ($circuit_id, $remote_id) = ($remote_id, $t_circuit_id);
  229. $dhcp_record->{'circuit-id'} = $circuit_id;
  230. $dhcp_record->{'remote-id'} = $remote_id;
  231. log_debug("Обнаружен MikroTik — поменяли местами circuit-id и remote-id");
  232. }
  233. }
  234. }
  235. # === ОПРЕДЕЛЕНИЕ ПОРТА ===
  236. if ($switch) {
  237. # Нормализуем circuit_id для поиска порта
  238. $t_circuit_id =~ s/[\+\-\s]+/ /g;
  239. # Загружаем порты коммутатора
  240. my @device_ports = get_records_sql($hdb, "SELECT * FROM device_ports WHERE device_id = " . $switch->{id});
  241. my %device_ports_h;
  242. foreach my $port_data (@device_ports) {
  243. $port_data->{snmp_index} //= $port_data->{port};
  244. $device_ports_h{$port_data->{port}} = $port_data;
  245. }
  246. # Пробуем найти порт по имени интерфейса (ifName)
  247. $switch_port = undef;
  248. foreach my $port_data (@device_ports) {
  249. if ($t_circuit_id =~ /\s*$port_data->{ifName}$/i ||
  250. $t_circuit_id =~ /^$port_data->{ifName}\s+/i) {
  251. $switch_port = $port_data;
  252. last;
  253. }
  254. }
  255. # Если не нашли по имени — пробуем hex-код (последние 2 байта)
  256. if (!$switch_port && $decoded_circuit_id) {
  257. my $hex_port = substr($decoded_circuit_id, -2);
  258. if ($hex_port && $hex_port =~ /^[0-9a-fA-F]{2}$/) {
  259. my $t_port = hex($hex_port);
  260. $switch_port = $device_ports_h{$t_port} if exists $device_ports_h{$t_port};
  261. log_debug("Порт определён по hex: $t_port") if $switch_port;
  262. }
  263. }
  264. # Запись лога и обновление подключения
  265. if ($switch_port) {
  266. db_log_verbose($hdb, "DHCP $type: IP=$ip, MAC=$mac " . $switch->{device_name} . " / " . $switch_port->{ifName});
  267. # Проверяем, существует ли уже соединение
  268. my $connection = get_records_sql($hdb, "SELECT * FROM connections WHERE auth_id = $auth_id");
  269. if (!$connection || !@{$connection}) {
  270. my $new_connection = {
  271. port_id => $switch_port->{id},
  272. device_id => $switch->{id},
  273. auth_id => $auth_id
  274. };
  275. insert_record($hdb, 'connections', $new_connection);
  276. log_debug("Создано новое соединение: auth_id=$auth_id");
  277. }
  278. } else {
  279. db_log_verbose($hdb, "DHCP $type: IP=$ip, MAC=$mac " . $switch->{device_name} . " (порт не определён)");
  280. log_warning("Не удалось определить порт для IP=$ip, коммутатор=" . $switch->{device_name});
  281. }
  282. }
  283. log_debug("Определён коммутатор: " . ($switch ? $switch->{device_name} : "НЕТ")) if $switch;
  284. log_debug("Определён порт: " . ($switch_port ? $switch_port->{ifName} : "НЕТ")) if $switch_port;
  285. }
  286. } # конец while чтения лога
  287. }; # конец eval
  288. # Обработка исключений
  289. if ($@) {
  290. log_error("Критическая ошибка в основном цикле: $@");
  291. sleep(60); # пауза перед повторной попыткой
  292. }
  293. } # конец while(1)
  294. }
  295. # === ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ===
  296. sub usage {
  297. print "usage: $MY_NAME (start|stop|status|restart)\n";
  298. exit(0);
  299. }
  300. sub reload {
  301. print "reload process not implemented.\n";
  302. log_warning("Команда 'reload' не поддерживается.");
  303. }
  304. sub restart {
  305. log_info("Запрошена перезагрузка демона...");
  306. stop();
  307. sleep(2);
  308. run();
  309. }