fetch_new_arp.pl 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. #!/usr/bin/perl
  2. #
  3. # Copyright (C) Roman Dmitriev, rnd@rajven.ru
  4. #
  5. use utf8;
  6. use open ":encoding(utf8)";
  7. use Encode;
  8. no warnings 'utf8';
  9. use English;
  10. use base;
  11. use FindBin '$Bin';
  12. use lib "/opt/Eye/scripts";
  13. use strict;
  14. use DBI;
  15. use Time::Local;
  16. use Net::Patricia;
  17. use Data::Dumper;
  18. use Date::Parse;
  19. use Socket;
  20. use eyelib::config;
  21. use eyelib::main;
  22. use eyelib::net_utils;
  23. use eyelib::snmp;
  24. use eyelib::database;
  25. use eyelib::common;
  26. use NetAddr::IP;
  27. use Fcntl qw(:flock);
  28. use Parallel::ForkManager;
  29. # Ensure only one instance of the script runs at a time
  30. open(SELF, "<", $0) or die "Cannot open $0 - $!";
  31. flock(SELF, LOCK_EX | LOCK_NB) or exit 1;
  32. # Lower process priority to minimize system impact
  33. setpriority(0, 0, 19);
  34. # Skip discovery if the system is in configuration mode
  35. if ($config_ref{config_mode}) {
  36. log_info("System in configuration mode! Skip discovery.");
  37. exit;
  38. }
  39. # Clean up empty user accounts and associated devices for dynamic users and hotspot
  40. db_log_verbose($dbh, 'Clearing empty records.');
  41. log_info($dbh, 'Clearing empty user accounts and associated devices for dynamic users and hotspot');
  42. my $u_sql = "SELECT * FROM user_list AS U WHERE (U.ou_id = " . $default_hotspot_ou_id . " OR U.ou_id = " . $default_user_ou_id . ") AND (SELECT COUNT(*) FROM user_auth WHERE user_auth.deleted = 0 AND user_auth.user_id = U.id) = 0";
  43. my @u_ref = get_records_sql($dbh, $u_sql);
  44. foreach my $row (@u_ref) {
  45. db_log_info($dbh, "Remove empty dynamic user with id: $row->{id} login: $row->{login}");
  46. delete_user($dbh, $row->{id});
  47. }
  48. # Clean up empty non-permanent user accounts that have no authentications or auth rules
  49. #if ($config_ref{clean_empty_user}) {
  50. # log_info($dbh, 'Clearing empty non-permanent user accounts and associated devices');
  51. # my $u_sql = "SELECT * FROM user_list AS U WHERE U.permanent = 0 AND (SELECT COUNT(*) FROM user_auth WHERE user_auth.deleted = 0 AND user_auth.user_id = U.id) = 0 AND (SELECT COUNT(*) FROM auth_rules WHERE auth_rules.user_id = U.id) = 0;";
  52. # my @u_ref = get_records_sql($dbh, $u_sql);
  53. # foreach my $row (@u_ref) {
  54. # db_log_info($dbh, "Remove empty user with id: $row->{id} login: $row->{login}");
  55. # delete_user($dbh, $row->{id});
  56. # }
  57. #}
  58. # Clean temporary (dynamic) user authentication records that have expired
  59. my $now = DateTime->now(time_zone => 'local');
  60. my $clear_time = $dbh->quote($now->strftime('%Y-%m-%d %H:%M:%S'));
  61. my $users_sql = "SELECT * FROM user_auth WHERE deleted = 0 AND dynamic = 1 AND eof <= " . $clear_time;
  62. my @users_auth = get_records_sql($dbh, $users_sql);
  63. if (@users_auth and scalar @users_auth) {
  64. foreach my $row (@users_auth) {
  65. delete_user_auth($dbh, $row->{id});
  66. db_log_info($dbh, "Removed dynamic user auth record for auth_id: $row->{'id'} by eof time: $row->{'eof'}", $row->{'id'});
  67. my $u_count = get_count_records($dbh, 'user_auth', 'deleted = 0 AND user_id = ' . $row->{user_id});
  68. if (!$u_count) {
  69. delete_user($dbh, $row->{'user_id'});
  70. }
  71. }
  72. }
  73. # Track MAC address history for change detection
  74. my %mac_history;
  75. # Get current timestamp components
  76. my ($sec, $min, $hour, $day, $month, $year, $zone) = localtime(time());
  77. $month += 1;
  78. $year += 1900;
  79. # Set parallelization level: 5 processes per CPU core
  80. my $fork_count = $cpu_count * 5;
  81. # Optional: disable forking during debugging (currently commented out)
  82. # if ($debug) { $fork_count = 0; }
  83. my $now_str = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $year, $month, $day, $hour, $min, $sec;
  84. my $now_day = sprintf "%04d-%02d-%02d", $year, $month, $day;
  85. db_log_verbose($dbh, 'ARP discovery started.');
  86. # If script is called with an argument, perform active ping-based network discovery
  87. if ($ARGV[0]) {
  88. db_log_verbose($dbh, 'Active network check started!');
  89. my $subnets = get_subnets_ref($dbh);
  90. my @fping_cmd = ();
  91. foreach my $net (keys %$subnets) {
  92. next if (!$net);
  93. next if (!$subnets->{$net}{discovery});
  94. my $run_cmd = "$fping -g $subnets->{$net}{subnet} -B1.0 -c 1 >/dev/null 2>&1";
  95. db_log_debug($dbh, "Checked network $subnets->{$net}{subnet}") if ($debug);
  96. push(@fping_cmd, $run_cmd);
  97. }
  98. $parallel_process_count = $cpu_count * 2;
  99. run_in_parallel(@fping_cmd);
  100. }
  101. # Fetch all SNMP-enabled routers and L3 switches eligible for ARP discovery
  102. my @router_ref = get_records_sql($dbh, "SELECT * FROM devices WHERE deleted = 0 AND (device_type = 2 OR device_type = 0) AND discovery = 1 AND snmp_version > 0 ORDER BY ip");
  103. # Release main DB handle before forking
  104. $dbh->disconnect;
  105. my @arp_array = ();
  106. # Initialize parallel manager for ARP table collection
  107. my $pm_arp = Parallel::ForkManager->new($fork_count);
  108. # Callback to collect ARP table results from child processes
  109. $pm_arp->run_on_finish(
  110. sub {
  111. my ($pid, $exit_code, $ident, $exit_signal, $core_dump, $data_structure_reference) = @_;
  112. if (defined($data_structure_reference)) {
  113. my $result = ${$data_structure_reference};
  114. push(@arp_array, $result);
  115. }
  116. }
  117. );
  118. # Iterate over each router and collect its ARP table in parallel
  119. DATA_LOOP:
  120. foreach my $router (@router_ref) {
  121. my $router_ip = $router->{ip};
  122. setCommunity($router);
  123. if (!HostIsLive($router_ip)) {
  124. log_info("Host id: $router->{id} name: $router->{device_name} ip: $router_ip is down! Skip.");
  125. next;
  126. }
  127. $pm_arp->start() and next DATA_LOOP;
  128. my $arp_table;
  129. my $tmp_dbh = init_db();
  130. if (apply_device_lock($tmp_dbh, $router->{id})) {
  131. $arp_table = get_arp_table($router_ip, $router->{snmp});
  132. unset_lock_discovery($tmp_dbh, $router->{id});
  133. }
  134. $tmp_dbh->disconnect;
  135. $pm_arp->finish(0, \$arp_table);
  136. }
  137. # Wait for all ARP collection processes to finish
  138. $pm_arp->wait_all_children;
  139. ########################### End ARP collection forks #########################
  140. # Reconnect to database after forking
  141. $dbh = init_db();
  142. # Load all active user authentications indexed by IP
  143. my @authlist_ref = get_records_sql($dbh, "SELECT * FROM user_auth WHERE deleted = 0 ORDER BY ip_int");
  144. my $users = Net::Patricia->new;
  145. my %ip_list;
  146. my %oper_arp_list;
  147. foreach my $row (@authlist_ref) {
  148. $users->add_string($row->{ip}, $row->{id});
  149. $ip_list{$row->{id}}->{id} = $row->{id};
  150. $ip_list{$row->{id}}->{ip} = $row->{ip};
  151. $ip_list{$row->{id}}->{mac} = mac_splitted($row->{mac}) || '';
  152. }
  153. # Process all collected ARP tables
  154. foreach my $arp_table (@arp_array) {
  155. foreach my $ip (keys %$arp_table) {
  156. next if (!$arp_table->{$ip});
  157. my $mac = trim($arp_table->{$ip});
  158. $mac = mac_splitted($mac);
  159. next if (!$mac);
  160. next if ($mac =~ /ff:ff:ff:ff:ff:ff/i); # Skip broadcast MAC
  161. next if ($mac !~ /(\S{2}):(\S{2}):(\S{2}):(\S{2}):(\S{2}):(\S{2})/); # Validate MAC format
  162. my $simple_mac = mac_simplify($mac);
  163. $ip = trim($ip);
  164. my $ip_aton = StrToIp($ip);
  165. # Initialize MAC history entry
  166. $mac_history{$simple_mac}{changed} = 0;
  167. $mac_history{$simple_mac}{ip} = $ip;
  168. $mac_history{$simple_mac}{auth_id} = 0;
  169. # Skip IPs outside configured office networks
  170. next if (!$office_networks->match_string($ip));
  171. db_log_debug($dbh, "Analyze ip: $ip mac: $mac") if ($debug);
  172. my $auth_id = $users->match_string($ip);
  173. my $arp_record;
  174. $arp_record->{ip} = $ip;
  175. $arp_record->{mac} = $mac;
  176. $arp_record->{type} = 'arp';
  177. $arp_record->{ip_aton} = $ip_aton;
  178. $arp_record->{hotspot} = is_hotspot($dbh, $ip);
  179. # Attempt to resurrect or map this ARP entry to a known auth record
  180. my $cur_auth_id = resurrection_auth($dbh, $arp_record);
  181. if (!$cur_auth_id) {
  182. db_log_warning($dbh, "Unknown record " . Dumper($arp_record));
  183. } else {
  184. $mac_history{$simple_mac}{auth_id} = $cur_auth_id;
  185. $arp_record->{auth_id} = $cur_auth_id;
  186. $arp_record->{updated} = 0;
  187. $oper_arp_list{$cur_auth_id} = $arp_record;
  188. # Mark as changed if IP-to-auth mapping differs from previous state
  189. if ($auth_id ne $cur_auth_id) {
  190. $mac_history{$simple_mac}{changed} = 1;
  191. }
  192. }
  193. }
  194. }
  195. db_log_verbose($dbh, 'MAC (FDB) discovery started.');
  196. # Load existing port connections for authenticated users
  197. my %connections = ();
  198. my @connections_list = get_records_sql($dbh, "SELECT * FROM connections ORDER BY auth_id");
  199. foreach my $connection (@connections_list) {
  200. next if (!$connection);
  201. $connections{$connection->{auth_id}}{port} = $connection->{port_id};
  202. $connections{$connection->{auth_id}}{id} = $connection->{id};
  203. }
  204. # Build operational and full MAC-to-auth lookup tables
  205. my $auth_filter = " AND last_found >= '" . $now_day . "' ";
  206. my $auth_sql = "SELECT id, mac FROM user_auth WHERE mac IS NOT NULL AND deleted = 0 $auth_filter ORDER BY id ASC";
  207. my @auth_list = get_records_sql($dbh, $auth_sql);
  208. my %auth_table;
  209. foreach my $auth (@auth_list) {
  210. next if (!$auth);
  211. my $auth_mac = mac_simplify($auth->{mac});
  212. $auth_table{oper_table}{$auth_mac} = $auth->{id};
  213. }
  214. $auth_sql = "SELECT id, mac FROM user_auth WHERE mac IS NOT NULL AND deleted = 0 ORDER BY last_found DESC";
  215. my @auth_full_list = get_records_sql($dbh, $auth_sql);
  216. foreach my $auth (@auth_full_list) {
  217. next if (!$auth);
  218. my $auth_mac = mac_simplify($auth->{mac});
  219. next if (exists $auth_table{full_table}{$auth_mac});
  220. $auth_table{full_table}{$auth_mac} = $auth->{id};
  221. }
  222. # Load unknown MAC addresses from the database
  223. my @unknown_list = get_records_sql($dbh, "SELECT id, mac, port_id, device_id FROM unknown_mac WHERE mac != '' ORDER BY mac");
  224. my %unknown_table;
  225. foreach my $unknown (@unknown_list) {
  226. next if (!$unknown);
  227. my $unknown_mac = mac_simplify($unknown->{mac});
  228. $unknown_table{$unknown_mac}{unknown_id} = $unknown->{id};
  229. $unknown_table{$unknown_mac}{port_id} = $unknown->{port_id};
  230. $unknown_table{$unknown_mac}{device_id} = $unknown->{device_id};
  231. }
  232. # Fetch all SNMP-enabled devices (switches, routers, etc.) for FDB discovery
  233. my @device_list = get_records_sql($dbh, "SELECT * FROM devices WHERE deleted = 0 AND discovery = 1 AND device_type <= 2 AND snmp_version > 0");
  234. my @fdb_array = ();
  235. # Initialize parallel manager for FDB (forwarding database) collection
  236. my $pm_fdb = Parallel::ForkManager->new($fork_count);
  237. # Callback to collect FDB results from child processes
  238. $pm_fdb->run_on_finish(
  239. sub {
  240. my ($pid, $exit_code, $ident, $exit_signal, $core_dump, $data_structure_reference) = @_;
  241. if (defined($data_structure_reference)) {
  242. my $result = ${$data_structure_reference};
  243. push(@fdb_array, $result);
  244. }
  245. }
  246. );
  247. # Release main DB handle before forking
  248. $dbh->disconnect;
  249. # Collect FDB tables from each device in parallel
  250. FDB_LOOP:
  251. foreach my $device (@device_list) {
  252. setCommunity($device);
  253. if (!HostIsLive($device->{ip})) {
  254. log_info("Host id: $device->{id} name: $device->{device_name} ip: $device->{ip} is down! Skip.");
  255. next;
  256. }
  257. my $int_list = get_snmp_ifindex($device->{ip}, $device->{snmp});
  258. if (!$int_list) {
  259. log_info("Host id: $device->{id} name: $device->{device_name} ip: $device->{ip} interfaces not found by SNMP request! Skip.");
  260. next;
  261. }
  262. $pm_fdb->start() and next FDB_LOOP;
  263. my $result;
  264. my $tmp_dbh = init_db();
  265. if (apply_device_lock($tmp_dbh, $device->{id})) {
  266. my $fdb = get_fdb_table($device->{ip}, $device->{snmp});
  267. unset_lock_discovery($tmp_dbh, $device->{id});
  268. $result->{id} = $device->{id};
  269. $result->{fdb} = $fdb;
  270. }
  271. $tmp_dbh->disconnect;
  272. $pm_fdb->finish(0, \$result);
  273. }
  274. # Wait for all FDB collection processes to finish
  275. $pm_fdb->wait_all_children;
  276. # Index FDB results by device ID
  277. my %fdb_ref;
  278. foreach my $fdb_table (@fdb_array) {
  279. next if (!$fdb_table);
  280. $fdb_ref{$fdb_table->{id}}{fdb} = $fdb_table->{fdb};
  281. }
  282. ################################ End FDB collection forks ##############################
  283. # Reconnect to database after forking
  284. $dbh = init_db();
  285. # Process FDB data for each device
  286. foreach my $device (@device_list) {
  287. my %port_snmp_index = (); # SNMP index → logical port number
  288. my %port_index = (); # logical port number → DB port ID
  289. my %mac_port_count = (); # port → number of learned MACs
  290. my %mac_address_table = (); # MAC → port
  291. my %port_links = (); # port → uplink/downlink target port ID
  292. my $dev_id = $device->{id};
  293. my $dev_name = $device->{device_name};
  294. my $fdb = $fdb_ref{$dev_id}{fdb};
  295. next if (!$fdb);
  296. # Load device port mappings from database
  297. my @device_ports = get_records_sql($dbh, "SELECT * FROM device_ports WHERE device_id = $dev_id");
  298. foreach my $port_data (@device_ports) {
  299. my $fdb_port_index = $port_data->{port};
  300. my $port_id = $port_data->{id};
  301. if (!$port_data->{snmp_index}) {
  302. $port_data->{snmp_index} = $port_data->{port};
  303. }
  304. $fdb_port_index = $port_data->{snmp_index};
  305. next if ($port_data->{skip});
  306. $port_snmp_index{$port_data->{snmp_index}} = $port_data->{port};
  307. $port_index{$port_data->{port}} = $port_id;
  308. $port_links{$port_data->{port}} = $port_data->{target_port_id};
  309. $mac_port_count{$port_data->{port}} = 0;
  310. }
  311. # Special handling for MikroTik: skip device's own MAC addresses
  312. my $sw_mac;
  313. if ($device->{vendor_id} eq '9') {
  314. my $sw_auth = get_record_sql($dbh, "SELECT mac FROM user_auth WHERE deleted = 0 AND ip = '" . $device->{ip} . "'");
  315. $sw_mac = mac_simplify($sw_auth->{mac});
  316. $sw_mac =~ s/.{2}$//s; # Strip last two hex chars for prefix match
  317. }
  318. # Process each MAC in the FDB
  319. foreach my $mac (keys %$fdb) {
  320. my $port = $fdb->{$mac};
  321. next if (!$port);
  322. # Resolve SNMP index to logical port number
  323. if (exists $port_snmp_index{$port}) {
  324. $port = $port_snmp_index{$port};
  325. }
  326. next if (!exists $port_index{$port});
  327. # Skip MikroTik's own MACs
  328. if ($sw_mac && $mac =~ /^$sw_mac/i) {
  329. next;
  330. }
  331. $mac_port_count{$port}++;
  332. $mac_address_table{$mac} = $port;
  333. }
  334. # Update MAC count per port in the database (skip uplinks/downlinks)
  335. foreach my $port (keys %mac_port_count) {
  336. next if (!$port || !exists $port_index{$port} || $port_links{$port} > 0);
  337. my $dev_ports;
  338. $dev_ports->{last_mac_count} = $mac_port_count{$port};
  339. update_record($dbh, 'device_ports', $dev_ports, "device_id = $dev_id AND port = $port");
  340. }
  341. # Process each learned MAC address
  342. foreach my $mac (keys %mac_address_table) {
  343. my $port = $mac_address_table{$mac};
  344. next if (!$port || !exists $port_index{$port} || $port_links{$port} > 0);
  345. my $simple_mac = mac_simplify($mac);
  346. my $mac_splitted = mac_splitted($mac);
  347. $mac_history{$simple_mac}{port_id} = $port_index{$port};
  348. $mac_history{$simple_mac}{dev_id} = $dev_id;
  349. $mac_history{$simple_mac}{changed} //= 0;
  350. my $port_id = $port_index{$port};
  351. # Case 1: MAC belongs to a known authenticated user
  352. if (exists $auth_table{full_table}{$simple_mac} || exists $auth_table{oper_table}{$simple_mac}) {
  353. my $auth_id = exists $auth_table{oper_table}{$simple_mac}
  354. ? $auth_table{oper_table}{$simple_mac}
  355. : $auth_table{full_table}{$simple_mac};
  356. unless (exists $auth_table{oper_table}{$simple_mac}) {
  357. db_log_debug($dbh, "MAC not found in current ARP table. Using historical auth_id: $auth_id [$simple_mac] at device $dev_name [$port]", $auth_id);
  358. }
  359. if (exists $connections{$auth_id}) {
  360. if ($port_id == $connections{$auth_id}{port}) {
  361. # No port change: just update last seen time if in current ARP
  362. if (exists $auth_table{oper_table}{$simple_mac}) {
  363. my $auth_rec;
  364. $auth_rec->{last_found} = $now_str;
  365. $auth_rec->{arp_found} = $now_str;
  366. $oper_arp_list{$auth_id}{updated} = 1;
  367. update_record($dbh, 'user_auth', $auth_rec, "id = $auth_id");
  368. }
  369. next;
  370. }
  371. # Port changed: update connection and log
  372. $connections{$auth_id}{port} = $port_id;
  373. $mac_history{$simple_mac}{changed} = 1;
  374. $mac_history{$simple_mac}{auth_id} = $auth_id;
  375. db_log_info($dbh, "Found auth_id: $auth_id ip: $mac_history{$simple_mac}{ip} [$mac_splitted] at device $dev_name [$port]. Update connection.", $auth_id);
  376. my $auth_rec;
  377. $auth_rec->{last_found} = $now_str;
  378. $auth_rec->{arp_found} = $now_str if exists $auth_table{oper_table}{$simple_mac};
  379. $oper_arp_list{$auth_id}{updated} = 1;
  380. update_record($dbh, 'user_auth', $auth_rec, "id = $auth_id");
  381. my $conn_rec;
  382. $conn_rec->{port_id} = $port_id;
  383. $conn_rec->{device_id} = $dev_id;
  384. update_record($dbh, 'connections', $conn_rec, "auth_id = $auth_id");
  385. } else {
  386. # New connection for known user
  387. $mac_history{$simple_mac}{changed} = 1;
  388. $mac_history{$simple_mac}{auth_id} = $auth_id;
  389. $connections{$auth_id}{port} = $port_id;
  390. db_log_info($dbh, "Found auth_id: $auth_id ip: $mac_history{$simple_mac}{ip} [$mac_splitted] at device $dev_name [$port]. Create connection.", $auth_id);
  391. my $auth_rec;
  392. $auth_rec->{last_found} = $now_str;
  393. $auth_rec->{arp_found} = $now_str if exists $auth_table{oper_table}{$simple_mac};
  394. $oper_arp_list{$auth_id}{updated} = 1;
  395. update_record($dbh, 'user_auth', $auth_rec, "id = $auth_id");
  396. my $conn_rec;
  397. $conn_rec->{port_id} = $port_id;
  398. $conn_rec->{device_id} = $dev_id;
  399. $conn_rec->{auth_id} = $auth_id;
  400. insert_record($dbh, 'connections', $conn_rec);
  401. }
  402. }
  403. # Case 2: MAC is unknown
  404. else {
  405. if (exists $unknown_table{$simple_mac}{unknown_id}) {
  406. # MAC already known but moved
  407. next if ($unknown_table{$simple_mac}{port_id} == $port_id && $unknown_table{$simple_mac}{device_id} == $dev_id);
  408. $mac_history{$simple_mac}{changed} = 1;
  409. $mac_history{$simple_mac}{auth_id} = 0;
  410. db_log_debug($dbh, "Unknown MAC $mac_splitted moved to $dev_name [$port]") if ($debug);
  411. my $unknown_rec;
  412. $unknown_rec->{port_id} = $port_id;
  413. $unknown_rec->{device_id} = $dev_id;
  414. update_record($dbh, 'unknown_mac', $unknown_rec, "id = $unknown_table{$simple_mac}{unknown_id}");
  415. } else {
  416. # Brand new unknown MAC
  417. $mac_history{$simple_mac}{changed} = 1;
  418. $mac_history{$simple_mac}{auth_id} = 0;
  419. db_log_debug($dbh, "Unknown MAC $mac_splitted found at $dev_name [$port]") if ($debug);
  420. my $unknown_rec;
  421. $unknown_rec->{port_id} = $port_id;
  422. $unknown_rec->{device_id} = $dev_id;
  423. $unknown_rec->{mac} = $simple_mac;
  424. insert_record($dbh, 'unknown_mac', $unknown_rec);
  425. }
  426. }
  427. }
  428. }
  429. # Ensure all active ARP-auth entries have their 'last_found' timestamp updated
  430. foreach my $auth_id (keys %oper_arp_list) {
  431. next if ($oper_arp_list{$auth_id}->{updated});
  432. my $auth_rec;
  433. $auth_rec->{last_found} = $now_str;
  434. update_record($dbh, 'user_auth', $auth_rec, "id = $auth_id");
  435. db_log_debug($dbh, "Update by ARP at unknown connection location for: " . Dumper($oper_arp_list{$auth_id})) if ($debug);
  436. }
  437. # Log all MAC movement/history events
  438. foreach my $mac (keys %mac_history) {
  439. next if (!$mac || !$mac_history{$mac}->{changed});
  440. my $h_dev_id = $mac_history{$mac}->{dev_id} || '';
  441. my $h_port_id = $mac_history{$mac}->{port_id} || '';
  442. my $h_ip = $mac_history{$mac}->{ip} || '';
  443. my $h_auth_id = $mac_history{$mac}->{auth_id} || 0;
  444. next if (!$h_dev_id);
  445. my $history_rec;
  446. $history_rec->{device_id} = $h_dev_id;
  447. $history_rec->{port_id} = $h_port_id;
  448. $history_rec->{mac} = $mac;
  449. $history_rec->{ip} = $h_ip;
  450. $history_rec->{auth_id} = $h_auth_id;
  451. insert_record($dbh, 'mac_history', $history_rec);
  452. }
  453. $dbh->disconnect;
  454. exit 0;