#!/usr/bin/perl # # Copyright (C) Roman Dmitriev, rnd@rajven.ru # use utf8; use warnings; use Encode; use open qw(:std :encoding(UTF-8)); no warnings 'utf8'; use English; use base; use FindBin '$Bin'; use lib "/opt/Eye/scripts"; use Data::Dumper; use eyelib::config; use eyelib::main; use eyelib::database; use eyelib::common; use eyelib::logconfig; use eyelib::net_utils; use strict; use Cwd; use Net::Netmask; use DateTime; my $mute_time=300; ### Analyze DHCP requests # === 1. Получение событий из очереди DHCP === sub _fetch_dhcp_queue { my ($dbh) = @_; my @events = get_records_sql($dbh, "SELECT * FROM dhcp_queue"); log_debug("Fetched " . scalar(@events) . " DHCP event(s) from queue") if @events; return @events; } # === 2. Проверка, нужно ли подавлять событие (mute logic) === sub _should_mute_dhcp_event { my ($ip, $new_action, $leases_ref, $mute_time) = @_; return 0 unless exists $leases_ref->{$ip}; my $last_event = $leases_ref->{$ip}; my $time_diff = time() - ($last_event->{last_time} // 0); # Подавляем, если: # - действие не отличается от предыдущего # - и прошло меньше $mute_time секунд if ($last_event->{action} eq $new_action && $time_diff <= $mute_time) { log_debug("Muting DHCP event for IP $ip: same as recent opposite action (diff: ${time_diff}s)"); return 1; } return 0; } # === 3. Обработка одного DHCP-события === sub _process_single_dhcp_event { my ($dbh, $event, $leases_ref, $mute_time) = @_; my $ip = $event->{ip}; my $action = $event->{action}; # Проверка на подавление if (_should_mute_dhcp_event($ip, $action, $leases_ref, $mute_time)) { # Всё равно обновляем last_time, чтобы не накапливать старые записи $leases_ref->{$ip} = $event; $leases_ref->{$ip}->{last_time} //= time(); return; } # Обработка запроса log_debug("Processing DHCP event: action=$action, ip=$ip, mac=$event->{mac}"); my $dhcp_record = process_dhcp_request( $dbh, $action, $event->{mac}, $ip, $event->{dhcp_hostname}, '', '', '' ); # Удаляем из очереди my $rows = do_sql($dbh, "DELETE FROM dhcp_queue WHERE id = ?", $event->{id}); log_debug("Deleted DHCP event ID $event->{id} from queue (affected rows: $rows)"); # Проверяем, что запись создана/обновлена if (!$dhcp_record or !$dhcp_record->{auth_id} ){ wrlog($W_ERROR,"User ip auth record not created by DHCP request for ip: $ip mac: $event->{mac}!"); return; } # Обновляем кэш последних событий $leases_ref->{$ip} = $event; $leases_ref->{$ip}->{last_time} //= time(); } # === 4. Основная функция обработки очереди DHCP === sub process_dhcp_queue { my ($hdb, $leases_ref, $mute_time) = @_; # Получаем все события my @dhcp_events = _fetch_dhcp_queue($hdb); return unless @dhcp_events; wrlog($W_INFO,"Processing " . scalar(@dhcp_events) . " DHCP event(s) from queue"); # Обрабатываем каждое событие foreach my $dhcp (@dhcp_events) { eval { _process_single_dhcp_event($hdb, $dhcp, $leases_ref, $mute_time); }; if ($@) { wrlog($W_ERROR,"Failed to process DHCP event ID $dhcp->{id}: $@"); # Не прерываем остальные события } } wrlog($W_INFO,"DHCP queue processing completed"); } ### UPDATE user state # === 1. Обнуление флагов changed для динамических/хостспот-пользователей === sub _reset_changed_flags_for_default_ous { my ($dbh, $default_user_ou_id, $default_hotspot_ou_id) = @_; if (!defined $default_user_ou_id || !defined $default_hotspot_ou_id) { log_warning("Skipping reset of changed flags: default OU IDs not set"); return; } my $rows1 = do_sql($dbh, "UPDATE user_auth SET changed = 0 WHERE ou_id = ? OR ou_id = ?", $default_user_ou_id, $default_hotspot_ou_id); my $rows2 = do_sql($dbh, "UPDATE user_auth SET dhcp_changed = 0 WHERE ou_id = ? OR ou_id = ?", $default_user_ou_id, $default_hotspot_ou_id); log_debug("Reset 'changed' flags for $rows1 records, 'dhcp_changed' for $rows2 records (default OUs)"); } # === 2. Сброс флагов changed для IP вне офисных сетей === sub _clear_changed_flags_for_non_office_ips { my ($dbh, $office_networks) = @_; my @all_changed = get_records_sql($dbh, "SELECT id, ip FROM user_auth WHERE changed = 1 OR dhcp_changed = 1"); return unless @all_changed; my $cleared = 0; for my $row (@all_changed) { next if !$row->{ip}; next if $office_networks->match_string($row->{ip}); # IP в офисной сети — оставляем my $rows = do_sql($dbh, "UPDATE user_auth SET changed = 0, dhcp_changed = 0 WHERE id = ?", $row->{id}); $cleared += $rows; } if ($cleared) { wrlog($W_INFO,"Cleared 'changed' flags for $cleared records with non-office IPs"); } } # === 3. Обработка DHCP-изменений === sub _process_dhcp_changes { my ($dbh) = @_; my $changed = get_record_sql($dbh, "SELECT COUNT(*) AS c_count FROM user_auth WHERE dhcp_changed = 1"); my $count = $changed ? ($changed->{c_count} // 0) : 0; return if $count == 0; wrlog($W_INFO,"Found $count record(s) with dhcp_changed=1"); # Сбрасываем флаги do_sql($dbh, "UPDATE user_auth SET dhcp_changed = 0"); # Запускаем внешний скрипт my $dhcp_exec = get_option($dbh, 38); if (!$dhcp_exec) { log_warning("DHCP sync script (opt 38) not configured"); return; } my %result = do_exec_ref("/usr/bin/sudo $dhcp_exec"); if ($result{status} != 0) { wrlog($W_ERROR,"DHCP config sync failed: " . ($result{stderr} // 'no error output')); } else { wrlog($W_INFO,"DHCP config synced successfully"); } } # === 4. Обработка ACL-изменений === sub _process_acl_changes { my ($dbh) = @_; my $changed = get_record_sql($dbh, "SELECT COUNT(*) AS c_count FROM user_auth WHERE changed = 1"); my $count = $changed ? ($changed->{c_count} // 0) : 0; return if $count == 0; wrlog($W_INFO,"Found $count record(s) with changed=1 (ACL/DHCP)"); my $acl_exec = get_option($dbh, 37); if (!$acl_exec) { log_warning("ACL sync script (opt 37) not configured"); return; } my %result = do_exec_ref("$acl_exec --changes-only"); if ($result{status} != 0) { wrlog($W_ERROR,"Gateway ACL sync failed: " . ($result{stderr} // 'no error output')); } else { wrlog($W_INFO,"Gateway ACL synced successfully"); } } # === 5. Обработка DNS-очереди === sub _process_dns_queue { my ($dbh) = @_; my @dns_changed = get_records_sql($dbh, "SELECT DISTINCT auth_id FROM dns_queue"); return unless @dns_changed; wrlog($W_INFO,"Processing DNS queue for " . scalar(@dns_changed) . " auth_id(s)"); for my $auth (@dns_changed) { my $auth_id = $auth->{auth_id}; eval { update_dns_record($dbh, $auth_id); do_sql($dbh, "DELETE FROM dns_queue WHERE auth_id = ?", $auth_id); wrlog($W_INFO,"DNS processed and cleared for auth_id: $auth_id"); }; if ($@) { wrlog($W_ERROR,"Failed to process DNS for auth_id=$auth_id: $@"); } } } # === 6. Очистка временных записей user_auth === sub _cleanup_expired_dynamic_users { my ($dbh) = @_; # Используем параметризованный запрос вместо quote() my $now_str = DateTime->now(time_zone => 'local')->strftime('%Y-%m-%d %H:%M:%S'); my @users_auth = get_records_sql($dbh, "SELECT id, user_id, end_life FROM user_auth WHERE deleted = 0 AND dynamic = 1 AND end_life <= ?", $now_str ); return unless @users_auth; wrlog($W_INFO,"Cleaning up " . scalar(@users_auth) . " expired dynamic user_auth records"); for my $row (@users_auth) { eval { delete_user_auth($dbh, $row->{id}); db_wrlog($W_INFO,$dbh, "Removed dynamic user auth record for auth_id: $row->{id} by end_life time: $row->{end_life}", $row->{id}); # Удаляем пользователя, если больше нет активных auth-записей my $u_count = get_count_records($dbh, 'user_auth', 'deleted = 0 AND user_id = ?', $row->{user_id}); if ($u_count == 0) { delete_user($dbh, $row->{user_id}); wrlog($W_INFO,"Deleted orphaned user_id: $row->{user_id}"); } }; if ($@) { wrlog($W_ERROR,"Error cleaning up auth_id $row->{id}: $@"); } } } # === 7. Основная функция обновления конфигурации === sub refresh_config_if_needed { my ($hdb, $last_refresh_ref, $default_user_ou_id, $default_hotspot_ou_id, $office_networks) = @_; return if time() - $$last_refresh_ref < 60; log_debug("Starting config refresh cycle"); # Обновляем опции init_option($hdb); my $urgent_sync = get_option($hdb, 50); if ($urgent_sync) { wrlog($W_INFO,"Urgent sync triggered (option 50)"); _reset_changed_flags_for_default_ous($hdb, $default_user_ou_id, $default_hotspot_ou_id); _clear_changed_flags_for_non_office_ips($hdb, $office_networks); _process_dhcp_changes($hdb); _process_acl_changes($hdb); } _process_dns_queue($hdb); _cleanup_expired_dynamic_users($hdb); $$last_refresh_ref = time(); log_debug("Config refresh cycle completed"); } wrlog($W_INFO,"Starting..."); setpriority(0,0,19); my %leases; while (1) { eval { # Create new database handle. If we can't connect, die() my $hdb = init_db(); # Process DHCP queue every 10 seconds process_dhcp_queue($hdb, \%leases, $mute_time); # Update state every 60 seconds refresh_config_if_needed( $hdb, \$last_refresh_config, $default_user_ou_id, $default_hotspot_ou_id, $office_networks ); sleep(10); }; if ($@) { wrlog($W_ERROR,"Exception found: $@"); sleep(300); } } wrlog($W_INFO,"Process stopped."); exit 0;