dhcp-log.pl 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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'} // 'N/A') .
  83. ", MAC=" . ($dhcp_event{'mac'} // 'N/A') .
  84. ", IP=" . ($dhcp_event{'ip'} // 'N/A') .
  85. ", NAME=" . ($dhcp_event{'hostname'} // 'N/A') .
  86. ", client-id=" . ($dhcp_event{'client_id'} // 'N/A') .
  87. ", circuit_id=" . ($dhcp_event{'decoded_circuit_id'} // 'N/A') .
  88. ", remote_id=" . ($dhcp_event{'decoded_remote_id'} // 'N/A'));
  89. # Suppress duplicate events within $mute_time window
  90. if (exists $leases{$dhcp_event{'ip'}} && $leases{$dhcp_event{'ip'}}{type} eq $dhcp_event{'type'} && (time() - $leases{$dhcp_event{'ip'}}{last_time} <= $mute_time)) {
  91. log_debug("Skipping duplicate: IP=$dhcp_event{'ip'}, type=$dhcp_event{'type'} (within $mute_time sec window)");
  92. next;
  93. }
  94. # Refresh config every 60 seconds
  95. if (time() - $last_refresh_config >= 60) {
  96. log_debug("Refreshing configuration...");
  97. init_option($hdb);
  98. }
  99. # Process DHCP request: update/create DB record
  100. 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'});
  101. next unless $dhcp_record;
  102. # Cache to suppress duplicates
  103. $leases{$dhcp_event{'ip'}} = {
  104. type => $dhcp_event{'type'},
  105. last_time => time()
  106. };
  107. my $auth_id = $dhcp_record->{auth_id};
  108. # === SWITCH AND PORT IDENTIFICATION LOGIC ===
  109. my ($switch, $switch_port);
  110. my ($t_remote_id, $t_circuit_id) = ($dhcp_event{'remote_id'}, $dhcp_event{'circuit_id'});
  111. # Only process connection events (add/old)
  112. if ($dhcp_event{'type'} =~ /^(add|old)$/i) {
  113. log_debug("Attempting to identify switch using Option 82 data...");
  114. # 1. Try decoded_remote_id as MAC address
  115. if ($dhcp_event{'$decoded_remote_id'}) {
  116. $t_remote_id = $dhcp_event{'$decoded_remote_id'};
  117. $t_remote_id .= "0" x (12 - length($t_remote_id)) if length($t_remote_id) < 12;
  118. $t_remote_id = mac_splitted(isc_mac_simplify($t_remote_id));
  119. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  120. "FROM devices AS D, user_auth AS A " .
  121. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  122. "AND A.mac = ?";
  123. $switch = get_record_sql($hdb, $devSQL, $t_remote_id);
  124. if ($switch) {
  125. $dhcp_event{'remote_id'} = $t_remote_id;
  126. $dhcp_event{'circuit_id'} = $dhcp_event{'decoded_circuit_id'};
  127. $dhcp_record->{circuit_id} = $dhcp_event{'circuit_id'};
  128. $dhcp_record->{remote_id} = $dhcp_event{'remote_id'};
  129. wrlog($W_INFO,"Switch found via decoded_remote_id: " . $switch->{device_name});
  130. }
  131. }
  132. # 2. If not found, try raw remote_id as MAC
  133. if (!$switch && $dhcp_event{'remote_id'}) {
  134. $t_remote_id = $dhcp_event{'remote_id'};
  135. $t_remote_id .= "0" x (12 - length($t_remote_id)) if length($t_remote_id) < 12;
  136. $t_remote_id = mac_splitted(isc_mac_simplify($t_remote_id));
  137. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  138. "FROM devices AS D, user_auth AS A " .
  139. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  140. "AND A.mac = ?";
  141. $switch = get_record_sql($hdb, $devSQL, $t_remote_id);
  142. if ($switch) {
  143. $dhcp_event{'remote_id'} = $t_remote_id;
  144. $dhcp_record->{circuit_id} = $dhcp_event{'circuit_id'};
  145. $dhcp_record->{remote_id} = $dhcp_event{'remote_id'};
  146. wrlog($W_INFO,"Switch found via remote_id: " . $switch->{device_name});
  147. }
  148. }
  149. # 3. If still not found, try remote_id as device name prefix
  150. if (!$switch && $dhcp_event{'remote_id'}) {
  151. my @id_words = split(/ /, $dhcp_event{'remote_id'});
  152. if ($id_words[0]) {
  153. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  154. "FROM devices AS D, user_auth AS A " .
  155. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  156. "AND D.device_name LIKE ?";
  157. $switch = get_record_sql($hdb, $devSQL, $id_words[0] . '%');
  158. if ($switch) {
  159. wrlog($W_INFO,"Switch found by name: " . $switch->{device_name});
  160. }
  161. }
  162. }
  163. # 4. Special case: MikroTik (circuit-id may contain device name)
  164. if (!$switch && $dhcp_event{'circuit_id'}) {
  165. my @id_words = split(/ /, $dhcp_event{'circuit_id'});
  166. if ($id_words[0]) {
  167. my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
  168. "FROM devices AS D, user_auth AS A " .
  169. "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
  170. "AND D.device_name LIKE ?";
  171. $switch = get_record_sql($hdb, $devSQL, $id_words[0] . '%');
  172. if ($switch) {
  173. # MikroTik often swaps circuit-id and remote-id
  174. ($dhcp_event{'circuit_id'}, $dhcp_event{'remote_id'}) = ($dhcp_event{'remote_id'}, $t_circuit_id);
  175. $dhcp_record->{circuit_id} = $dhcp_event{'circuit_id'};
  176. $dhcp_record->{remote_id} = $dhcp_event{'remote_id'};
  177. log_debug("Detected MikroTik — swapped circuit-id and remote-id");
  178. }
  179. }
  180. }
  181. # === LOG IF NO SWITCH MATCH FOUND ===
  182. unless ($switch) {
  183. 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'}'");
  184. }
  185. # === PORT IDENTIFICATION ===
  186. if ($switch) {
  187. # Normalize circuit_id for port matching
  188. $t_circuit_id =~ s/[\+\-\s]+/ /g;
  189. # Load switch ports
  190. my @device_ports = get_records_sql($hdb, "SELECT * FROM device_ports WHERE device_id = ?", $switch->{id});
  191. my %device_ports_h;
  192. foreach my $port_data (@device_ports) {
  193. $port_data->{snmp_index} //= $port_data->{port};
  194. $device_ports_h{$port_data->{port}} = $port_data;
  195. }
  196. # Try to match by interface name (ifName)
  197. $switch_port = undef;
  198. foreach my $port_data (@device_ports) {
  199. if ($t_circuit_id =~ /\s*$port_data->{ifname}$/i ||
  200. $t_circuit_id =~ /^$port_data->{ifname}\s+/i) {
  201. $switch_port = $port_data;
  202. last;
  203. }
  204. }
  205. # If not found by name, try hex port (last 2 bytes of decoded_circuit_id)
  206. if (!$switch_port && $dhcp_event{'decoded_circuit_id'}) {
  207. my $hex_port = substr($dhcp_event{'decoded_circuit_id'}, -2);
  208. if ($hex_port && $hex_port =~ /^[0-9a-fA-F]{2}$/) {
  209. my $t_port = hex($hex_port);
  210. $switch_port = $device_ports_h{$t_port} if exists $device_ports_h{$t_port};
  211. wrlog($W_INFO,"Port identified via hex: $t_port") if $switch_port;
  212. }
  213. }
  214. # Log and update connection
  215. if ($switch_port) {
  216. db_log_verbose($hdb, "DHCP $dhcp_event{'type'}: IP=$dhcp_event{'ip'}, MAC=$dhcp_event{'mac'} " . $switch->{device_name} . " / " . $switch_port->{ifname});
  217. # Check if connection already exists
  218. my $connection = get_records_sql($hdb, "SELECT * FROM connections WHERE auth_id = ?", $auth_id);
  219. if (!$connection || !@{$connection}) {
  220. my $new_connection = {
  221. port_id => $switch_port->{id},
  222. device_id => $switch->{id},
  223. auth_id => $auth_id
  224. };
  225. insert_record($hdb, 'connections', $new_connection);
  226. log_debug("New connection created: auth_id=$auth_id");
  227. }
  228. } else {
  229. db_log_verbose($hdb, "DHCP $dhcp_event{'type'}: IP=$dhcp_event{'ip'}, MAC=$dhcp_event{'mac'} " . $switch->{device_name} . " (port not identified)");
  230. wrlog($W_INFO,"Failed to identify port for IP=$dhcp_event{'ip'} on switch=" . $switch->{device_name});
  231. }
  232. }
  233. log_verbose("Identified Switch: " . ($switch ? $switch->{device_name} : "NONE") . " Port : " . ($switch_port ? $switch_port->{ifname} : "NONE"));
  234. }
  235. } # end while log reading
  236. }; # end eval
  237. # Exception handling
  238. if ($@) {
  239. wrlog($W_ERROR,"Critical error in main loop: $@");
  240. sleep(60);
  241. }
  242. } # end while(1)
  243. wrlog($W_INFO,"Process stopped.");
  244. exit;