dhcp-log.pl 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. #!/usr/bin/perl
  2. #
  3. # Author: Roman Dmitriev <rnd@rajven.ru>
  4. # Purpose: Script to parse DHCP logs, detect client connections via switches
  5. # using DHCP Option 82 (remote-id / circuit-id), and store link data in the database.
  6. #
  7. use utf8;
  8. use warnings;
  9. use Encode;
  10. use open qw(:std :encoding(UTF-8));
  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 eyelib::logconfig;
  23. use strict;
  24. use POSIX;
  25. use Net::Netmask;
  26. use Text::Iconv;
  27. use File::Tail;
  28. use Fcntl qw(:flock);
  29. # === LOCKING AND INITIALIZATION ===
  30. # Prevent multiple instances of the script
  31. open(SELF, "<", $0) or die "Cannot open $0 - $!";
  32. flock(SELF, LOCK_EX | LOCK_NB) or exit 1;
  33. # Set low process priority (nice = 19)
  34. setpriority(0, 0, 19);
  35. # === GLOBAL VARIABLES ===
  36. my $mute_time = 300; # Time (in seconds) to suppress duplicate DHCP events
  37. my $log_file = '/var/log/dhcp.log';
  38. wrlog($W_INFO,"Starting main DHCP log processing loop...");
  39. setpriority(0, 0, 19); # Ensure priority is set in child process
  40. # Converter for legacy cp866-encoded logs
  41. my $converter = Text::Iconv->new("cp866", "utf8");
  42. # Main infinite log-processing loop
  43. while (1) {
  44. eval {
  45. log_debug("Starting new DHCP log processing cycle.");
  46. my %leases; # cache to suppress duplicates
  47. # Establish fresh DB connection
  48. my $hdb = init_db();
  49. log_debug("Database connection established.");
  50. # Open log file for tail-like reading
  51. my $dhcp_log = File::Tail->new(
  52. name => $log_file,
  53. maxinterval => 5,
  54. interval => 1,
  55. ignore_nonexistent => 1
  56. ) || die "$log_file not found!";
  57. wrlog($W_INFO,"Beginning to read logs from $log_file...");
  58. while (my $logline = $dhcp_log->read) {
  59. next unless $logline;
  60. chomp($logline);
  61. wrlog($W_INFO,"Log line received: $logline");
  62. # Remove non-printable characters (keep letters, digits, punctuation, whitespace)
  63. $logline =~ s/[^\p{L}\p{N}\p{P}\p{Z}]//g;
  64. log_debug("Line after filtering: $logline");
  65. my @field_names = qw(
  66. type mac ip hostname timestamp
  67. tags sup_hostname old_hostname
  68. circuit_id remote_id client_id
  69. decoded_circuit_id decoded_remote_id
  70. );
  71. # Parse fields by semicolon
  72. my @values = split(/;/, $logline);
  73. my %dhcp_event;
  74. log_verbose("GET::");
  75. @dhcp_event{@field_names} = @values;
  76. for my $name (@field_names) {
  77. my $val = defined $dhcp_event{$name} ? $dhcp_event{$name} : '';
  78. log_verbose("Param '$name': $val");
  79. }
  80. # Skip lines without valid event type
  81. next unless $dhcp_event{'type'} && $dhcp_event{'type'} =~ /^(old|add|del)$/i;
  82. wrlog($W_INFO,"Processing event: type='$dhcp_event{'type'}', MAC='$dhcp_event{'mac'}', IP='$dhcp_event{'ip'}', NAME='$dhcp_event{'hostname'}', client-id='$dhcp_event{'client_id'}', circuit_id='$dhcp_event{'decoded_circuit_id'}', remote_id='$dhcp_event{'$decoded_remote_id'}'");
  83. # Suppress duplicate events within $mute_time window
  84. if (exists $leases{$dhcp_event{'ip'}} && $leases{$dhcp_event{'ip'}}{type} eq $dhcp_event{'type'} && (time() - $leases{$dhcp_event{'ip'}}{last_time} <= $mute_time)) {
  85. log_debug("Skipping duplicate: IP=$dhcp_event{'ip'}, type=$dhcp_event{'type'} (within $mute_time sec window)");
  86. next;
  87. }
  88. # Refresh config every 60 seconds
  89. if (time() - $last_refresh_config >= 60) {
  90. log_debug("Refreshing configuration...");
  91. init_option($hdb);
  92. }
  93. # Process DHCP request: update/create DB record
  94. my $dhcp_record = process_dhcp_request($hdb, $dhcp_event{'type'}, $dhcp_event{'mac'}, $dhcp_event{'ip'}, $dhcp_event{'hostname'}, $dhcp_event{'client_id'}, $dhcp_event{'decoded_circuit_id'}, $dhcp_event{'$decoded_remote_id'});
  95. next unless $dhcp_record;
  96. # Cache to suppress duplicates
  97. $leases{$dhcp_event{'ip'}} = {
  98. type => $dhcp_event{'type'},
  99. last_time => time()
  100. };
  101. my $auth_id = $dhcp_record->{auth_id};
  102. # === SWITCH AND PORT IDENTIFICATION LOGIC ===
  103. my ($switch, $switch_port);
  104. my ($t_remote_id, $t_circuit_id) = ($dhcp_event{'remote_id'}, $dhcp_event{'circuit_id'});
  105. # Only process connection events (add/old)
  106. if ($dhcp_event{'type'} =~ /^(add|old)$/i) {
  107. log_debug("Attempting to identify switch using Option 82 data...");
  108. # 1. Try decoded_remote_id as MAC address
  109. if ($dhcp_event{'$decoded_remote_id'}) {
  110. $t_remote_id = $dhcp_event{'$decoded_remote_id'};
  111. $t_remote_id .= "0" x (12 - length($t_remote_id)) if length($t_remote_id) < 12;
  112. $t_remote_id = mac_splitted(isc_mac_simplify($t_remote_id));
  113. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  114. "FROM devices AS D, user_auth AS A " .
  115. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  116. "AND A.mac = ?";
  117. $switch = get_record_sql($hdb, $devSQL, $t_remote_id);
  118. if ($switch) {
  119. $dhcp_event{'remote_id'} = $t_remote_id;
  120. $dhcp_event{'circuit_id'} = $dhcp_event{'decoded_circuit_id'};
  121. $dhcp_record->{circuit_id} = $dhcp_event{'circuit_id'};
  122. $dhcp_record->{remote_id} = $dhcp_event{'remote_id'};
  123. wrlog($W_INFO,"Switch found via decoded_remote_id: " . $switch->{device_name});
  124. }
  125. }
  126. # 2. If not found, try raw remote_id as MAC
  127. if (!$switch && $dhcp_event{'remote_id'}) {
  128. $t_remote_id = $dhcp_event{'remote_id'};
  129. $t_remote_id .= "0" x (12 - length($t_remote_id)) if length($t_remote_id) < 12;
  130. $t_remote_id = mac_splitted(isc_mac_simplify($t_remote_id));
  131. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  132. "FROM devices AS D, user_auth AS A " .
  133. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  134. "AND A.mac = ?";
  135. $switch = get_record_sql($hdb, $devSQL, $t_remote_id);
  136. if ($switch) {
  137. $dhcp_event{'remote_id'} = $t_remote_id;
  138. $dhcp_record->{circuit_id} = $dhcp_event{'circuit_id'};
  139. $dhcp_record->{remote_id} = $dhcp_event{'remote_id'};
  140. wrlog($W_INFO,"Switch found via remote_id: " . $switch->{device_name});
  141. }
  142. }
  143. # 3. If still not found, try remote_id as device name prefix
  144. if (!$switch && $dhcp_event{'remote_id'}) {
  145. my @id_words = split(/ /, $dhcp_event{'remote_id'});
  146. if ($id_words[0]) {
  147. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  148. "FROM devices AS D, user_auth AS A " .
  149. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  150. "AND D.device_name LIKE ?";
  151. $switch = get_record_sql($hdb, $devSQL, $id_words[0] . '%');
  152. if ($switch) {
  153. wrlog($W_INFO,"Switch found by name: " . $switch->{device_name});
  154. }
  155. }
  156. }
  157. # 4. Special case: MikroTik (circuit-id may contain device name)
  158. if (!$switch && $dhcp_event{'circuit_id'}) {
  159. my @id_words = split(/ /, $dhcp_event{'circuit_id'});
  160. if ($id_words[0]) {
  161. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  162. "FROM devices AS D, user_auth AS A " .
  163. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  164. "AND D.device_name LIKE ?";
  165. $switch = get_record_sql($hdb, $devSQL, $id_words[0] . '%');
  166. if ($switch) {
  167. # MikroTik often swaps circuit-id and remote-id
  168. ($dhcp_event{'circuit_id'}, $dhcp_event{'remote_id'}) = ($dhcp_event{'remote_id'}, $t_circuit_id);
  169. $dhcp_record->{circuit_id} = $dhcp_event{'circuit_id'};
  170. $dhcp_record->{remote_id} = $dhcp_event{'remote_id'};
  171. log_debug("Detected MikroTik — swapped circuit-id and remote-id");
  172. }
  173. }
  174. }
  175. # === LOG IF NO SWITCH MATCH FOUND ===
  176. unless ($switch) {
  177. wrlog($W_INFO,"No matching switch found for DHCP event: IP=$dhcp_event{'ip'}, MAC=$dhcp_event{'mac'}, remote_id='$dhcp_event{'remote_id'}', circuit_id='$dhcp_event{'circuit_id'}'");
  178. }
  179. # === PORT IDENTIFICATION ===
  180. if ($switch) {
  181. # Normalize circuit_id for port matching
  182. $t_circuit_id =~ s/[\+\-\s]+/ /g;
  183. # Load switch ports
  184. my @device_ports = get_records_sql($hdb, "SELECT * FROM device_ports WHERE device_id = ?", $switch->{id});
  185. my %device_ports_h;
  186. foreach my $port_data (@device_ports) {
  187. $port_data->{snmp_index} //= $port_data->{port};
  188. $device_ports_h{$port_data->{port}} = $port_data;
  189. }
  190. # Try to match by interface name (ifName)
  191. $switch_port = undef;
  192. foreach my $port_data (@device_ports) {
  193. if ($t_circuit_id =~ /\s*$port_data->{ifname}$/i ||
  194. $t_circuit_id =~ /^$port_data->{ifname}\s+/i) {
  195. $switch_port = $port_data;
  196. last;
  197. }
  198. }
  199. # If not found by name, try hex port (last 2 bytes of decoded_circuit_id)
  200. if (!$switch_port && $dhcp_event{'decoded_circuit_id'}) {
  201. my $hex_port = substr($dhcp_event{'decoded_circuit_id'}, -2);
  202. if ($hex_port && $hex_port =~ /^[0-9a-fA-F]{2}$/) {
  203. my $t_port = hex($hex_port);
  204. $switch_port = $device_ports_h{$t_port} if exists $device_ports_h{$t_port};
  205. wrlog($W_INFO,"Port identified via hex: $t_port") if $switch_port;
  206. }
  207. }
  208. # Log and update connection
  209. if ($switch_port) {
  210. db_log_verbose($hdb, "DHCP $dhcp_event{'type'}: IP=$dhcp_event{'ip'}, MAC=$dhcp_event{'mac'} " . $switch->{device_name} . " / " . $switch_port->{ifname});
  211. # Check if connection already exists
  212. my $connection = get_records_sql($hdb, "SELECT * FROM connections WHERE auth_id = ?", $auth_id);
  213. if (!$connection || !@{$connection}) {
  214. my $new_connection = {
  215. port_id => $switch_port->{id},
  216. device_id => $switch->{id},
  217. auth_id => $auth_id
  218. };
  219. insert_record($hdb, 'connections', $new_connection);
  220. log_debug("New connection created: auth_id=$auth_id");
  221. }
  222. } else {
  223. db_log_verbose($hdb, "DHCP $dhcp_event{'type'}: IP=$dhcp_event{'ip'}, MAC=$dhcp_event{'mac'} " . $switch->{device_name} . " (port not identified)");
  224. wrlog($W_INFO,"Failed to identify port for IP=$dhcp_event{'ip'} on switch=" . $switch->{device_name});
  225. }
  226. }
  227. log_verbose("Identified Switch: " . ($switch ? $switch->{device_name} : "NONE") . " Port : " . ($switch_port ? $switch_port->{ifname} : "NONE"));
  228. }
  229. } # end while log reading
  230. }; # end eval
  231. # Exception handling
  232. if ($@) {
  233. wrlog($W_ERROR,"Critical error in main loop: $@");
  234. sleep(60);
  235. }
  236. } # end while(1)
  237. wrlog($W_INFO,"Process stopped.");
  238. exit;