dhcp-log.pl 17 KB

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