| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- #!/usr/bin/perl
- #
- # Author: Roman Dmitriev <rnd@rajven.ru>
- # Purpose: Script to parse DHCP logs, detect client connections via switches
- # using DHCP Option 82 (remote-id / circuit-id), and store link data in the database.
- #
- 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::net_utils;
- use strict;
- use Getopt::Long;
- use Proc::Daemon;
- use POSIX;
- use Net::Netmask;
- use Text::Iconv;
- use File::Tail;
- use Fcntl qw(:flock);
- # === LOCKING AND INITIALIZATION ===
- # Prevent multiple instances of the script
- open(SELF, "<", $0) or die "Cannot open $0 - $!";
- flock(SELF, LOCK_EX | LOCK_NB) or exit 1;
- # Set low process priority (nice = 19)
- setpriority(0, 0, 19);
- # === GLOBAL VARIABLES ===
- my $mute_time = 300; # Time (in seconds) to suppress duplicate DHCP events
- my $log_file = '/var/log/dhcp.log';
- # Determine process name and PID file
- my $proc_name = $MY_NAME;
- $proc_name =~ s/\.[^.]+$//;
- my $pid_file = '/run/eye/' . $proc_name;
- my $pf = $pid_file . '.pid';
- # Daemon setup
- my $daemon = Proc::Daemon->new(
- pid_file => $pf,
- work_dir => $HOME_DIR
- );
- # Check if process is already running
- my $pid = $daemon->Status($pf);
- my $daemonize = 1; # Default: run in background
- # === COMMAND-LINE ARGUMENT HANDLING ===
- GetOptions(
- 'daemon!' => \$daemonize,
- "help" => \&usage,
- "reload" => \&reload,
- "restart" => \&restart,
- "start" => \&run,
- "status" => \&status,
- "stop" => \&stop
- ) or &usage;
- exit(0);
- # === DAEMON CONTROL FUNCTIONS ===
- sub stop {
- log_info("Stop requested...");
- if ($pid) {
- print "Stopping pid $pid...";
- if ($daemon->Kill_Daemon($pf)) {
- print "Successfully stopped.\n";
- log_info("Daemon stopped successfully (PID $pid).");
- } else {
- print "Could not find $pid. Was it running?\n";
- log_warning("Failed to stop process PID $pid — possibly already terminated.");
- }
- } else {
- print "Not running, nothing to stop.\n";
- log_info("Daemon is not running — nothing to stop.");
- }
- }
- sub status {
- if ($pid) {
- print "Running with pid $pid.\n";
- log_info("Status: daemon is running (PID $pid).");
- } else {
- print "Not running.\n";
- log_info("Status: daemon is not running.");
- }
- }
- sub run {
- log_info("Starting main DHCP log processing loop...");
- if ($pid) {
- print "Already Running with pid $pid\n";
- log_warning("Attempt to start already running daemon (PID $pid).");
- return;
- }
- print "Starting...\n";
- log_info("Initializing daemon...");
- if ($daemonize) {
- $daemon->Init;
- log_debug("Daemon initialized in background mode.");
- }
- setpriority(0, 0, 19); # Ensure priority is set in child process
- # Converter for legacy cp866-encoded logs
- my $converter = Text::Iconv->new("cp866", "utf8");
- # Main infinite log-processing loop
- while (1) {
- eval {
- log_debug("Starting new DHCP log processing cycle.");
- my %leases; # cache to suppress duplicates
- # Establish fresh DB connection
- my $hdb = init_db();
- log_debug("Database connection established.");
- # Open log file for tail-like reading
- my $dhcp_log = File::Tail->new(
- name => $log_file,
- maxinterval => 5,
- interval => 1,
- ignore_nonexistent => 1
- ) || die "$log_file not found!";
- log_info("Beginning to read logs from $log_file...");
- while (my $logline = $dhcp_log->read) {
- next unless $logline;
- chomp($logline);
- log_info("Log line received: $logline");
- # Remove non-printable characters (keep letters, digits, punctuation, whitespace)
- $logline =~ s/[^\p{L}\p{N}\p{P}\p{Z}]//g;
- log_debug("Line after filtering: $logline");
- my @field_names = qw(
- type mac ip hostname timestamp
- tags sup_hostname old_hostname
- circuit_id remote_id client_id
- decoded_circuit_id decoded_remote_id
- );
- # Parse fields by semicolon
- my @values = split(/;/, $logline);
- my %dhcp_event;
- log_verbose("GET::");
- @dhcp_event{@field_names} = @values;
- for my $name (@field_names) {
- my $val = defined $dhcp_event{$name} ? $dhcp_event{$name} : '';
- log_verbose("Param '$name': $val");
- }
- # Skip lines without valid event type
- next unless $dhcp_event{'type'} && $dhcp_event{'type'} =~ /^(old|add|del)$/i;
- log_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'}'");
- # Suppress duplicate events within $mute_time window
- if (exists $leases{$dhcp_event{'ip'}} && $leases{$dhcp_event{'ip'}}{type} eq $dhcp_event{'type'} && (time() - $leases{$dhcp_event{'ip'}}{last_time} <= $mute_time)) {
- log_debug("Skipping duplicate: IP=$dhcp_event{'ip'}, type=$dhcp_event{'type'} (within $mute_time sec window)");
- next;
- }
- # Refresh config every 60 seconds
- if (time() - $last_refresh_config >= 60) {
- log_debug("Refreshing configuration...");
- init_option($hdb);
- }
- # Process DHCP request: update/create DB record
- 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'});
- next unless $dhcp_record;
- # Cache to suppress duplicates
- $leases{$dhcp_event{'ip'}} = {
- type => $dhcp_event{'type'},
- last_time => time()
- };
- my $auth_id = $dhcp_record->{auth_id};
- # === SWITCH AND PORT IDENTIFICATION LOGIC ===
- my ($switch, $switch_port);
- my ($t_remote_id, $t_circuit_id) = ($dhcp_event{'remote_id'}, $dhcp_event{'circuit_id'});
- # Only process connection events (add/old)
- if ($dhcp_event{'type'} =~ /^(add|old)$/i) {
- log_debug("Attempting to identify switch using Option 82 data...");
- # 1. Try decoded_remote_id as MAC address
- if ($dhcp_event{'$decoded_remote_id'}) {
- $t_remote_id = $dhcp_event{'$decoded_remote_id'};
- $t_remote_id .= "0" x (12 - length($t_remote_id)) if length($t_remote_id) < 12;
- $t_remote_id = mac_splitted(isc_mac_simplify($t_remote_id));
- my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
- "FROM devices AS D, user_auth AS A " .
- "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
- "AND A.mac = ?";
- $switch = get_record_sql($hdb, $devSQL, $t_remote_id);
- if ($switch) {
- $dhcp_event{'remote_id'} = $t_remote_id;
- $dhcp_event{'circuit_id'} = $dhcp_event{'decoded_circuit_id'};
- $dhcp_record->{circuit_id} = $dhcp_event{'circuit_id'};
- $dhcp_record->{remote_id} = $dhcp_event{'remote_id'};
- log_info("Switch found via decoded_remote_id: " . $switch->{device_name});
- }
- }
- # 2. If not found, try raw remote_id as MAC
- if (!$switch && $dhcp_event{'remote_id'}) {
- $t_remote_id = $dhcp_event{'remote_id'};
- $t_remote_id .= "0" x (12 - length($t_remote_id)) if length($t_remote_id) < 12;
- $t_remote_id = mac_splitted(isc_mac_simplify($t_remote_id));
- my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
- "FROM devices AS D, user_auth AS A " .
- "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
- "AND A.mac = ?";
- $switch = get_record_sql($hdb, $devSQL, $t_remote_id);
- if ($switch) {
- $dhcp_event{'remote_id'} = $t_remote_id;
- $dhcp_record->{circuit_id} = $dhcp_event{'circuit_id'};
- $dhcp_record->{remote_id} = $dhcp_event{'remote_id'};
- log_info("Switch found via remote_id: " . $switch->{device_name});
- }
- }
- # 3. If still not found, try remote_id as device name prefix
- if (!$switch && $dhcp_event{'remote_id'}) {
- my @id_words = split(/ /, $dhcp_event{'remote_id'});
- if ($id_words[0]) {
- my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
- "FROM devices AS D, user_auth AS A " .
- "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
- "AND D.device_name LIKE ?";
- $switch = get_record_sql($hdb, $devSQL, $id_words[0] . '%');
- if ($switch) {
- log_info("Switch found by name: " . $switch->{device_name});
- }
- }
- }
- # 4. Special case: MikroTik (circuit-id may contain device name)
- if (!$switch && $dhcp_event{'circuit_id'}) {
- my @id_words = split(/ /, $dhcp_event{'circuit_id'});
- if ($id_words[0]) {
- my $devSQL = "SELECT D.id, D.device_name, D.ip, A.mac " .
- "FROM devices AS D, user_auth AS A " .
- "WHERE D.user_id = A.User_id AND D.ip = A.ip AND A.deleted = 0 " .
- "AND D.device_name LIKE ?";
- $switch = get_record_sql($hdb, $devSQL, $id_words[0] . '%');
- if ($switch) {
- # MikroTik often swaps circuit-id and remote-id
- ($dhcp_event{'circuit_id'}, $dhcp_event{'remote_id'}) = ($dhcp_event{'remote_id'}, $t_circuit_id);
- $dhcp_record->{circuit_id} = $dhcp_event{'circuit_id'};
- $dhcp_record->{remote_id} = $dhcp_event{'remote_id'};
- log_debug("Detected MikroTik — swapped circuit-id and remote-id");
- }
- }
- }
- # === LOG IF NO SWITCH MATCH FOUND ===
- unless ($switch) {
- log_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'}'");
- }
- # === PORT IDENTIFICATION ===
- if ($switch) {
- # Normalize circuit_id for port matching
- $t_circuit_id =~ s/[\+\-\s]+/ /g;
- # Load switch ports
- my @device_ports = get_records_sql($hdb, "SELECT * FROM device_ports WHERE device_id = ?", $switch->{id});
- my %device_ports_h;
- foreach my $port_data (@device_ports) {
- $port_data->{snmp_index} //= $port_data->{port};
- $device_ports_h{$port_data->{port}} = $port_data;
- }
- # Try to match by interface name (ifName)
- $switch_port = undef;
- foreach my $port_data (@device_ports) {
- if ($t_circuit_id =~ /\s*$port_data->{ifname}$/i ||
- $t_circuit_id =~ /^$port_data->{ifname}\s+/i) {
- $switch_port = $port_data;
- last;
- }
- }
- # If not found by name, try hex port (last 2 bytes of decoded_circuit_id)
- if (!$switch_port && $dhcp_event{'decoded_circuit_id'}) {
- my $hex_port = substr($dhcp_event{'decoded_circuit_id'}, -2);
- if ($hex_port && $hex_port =~ /^[0-9a-fA-F]{2}$/) {
- my $t_port = hex($hex_port);
- $switch_port = $device_ports_h{$t_port} if exists $device_ports_h{$t_port};
- log_info("Port identified via hex: $t_port") if $switch_port;
- }
- }
- # Log and update connection
- if ($switch_port) {
- db_log_verbose($hdb, "DHCP $dhcp_event{'type'}: IP=$dhcp_event{'ip'}, MAC=$dhcp_event{'mac'} " . $switch->{device_name} . " / " . $switch_port->{ifname});
- # Check if connection already exists
- my $connection = get_records_sql($hdb, "SELECT * FROM connections WHERE auth_id = ?", $auth_id);
- if (!$connection || !@{$connection}) {
- my $new_connection = {
- port_id => $switch_port->{id},
- device_id => $switch->{id},
- auth_id => $auth_id
- };
- insert_record($hdb, 'connections', $new_connection);
- log_debug("New connection created: auth_id=$auth_id");
- }
- } else {
- db_log_verbose($hdb, "DHCP $dhcp_event{'type'}: IP=$dhcp_event{'ip'}, MAC=$dhcp_event{'mac'} " . $switch->{device_name} . " (port not identified)");
- log_info("Failed to identify port for IP=$dhcp_event{'ip'} on switch=" . $switch->{device_name});
- }
- }
- log_verbose("Identified Switch: " . ($switch ? $switch->{device_name} : "NONE") . " Port : " . ($switch_port ? $switch_port->{ifname} : "NONE"));
- }
- } # end while log reading
- }; # end eval
- # Exception handling
- if ($@) {
- log_error("Critical error in main loop: $@");
- sleep(60); # pause before retry
- }
- } # end while(1)
- }
- # === HELPER FUNCTIONS ===
- sub usage {
- print "usage: $MY_NAME (start|stop|status|restart)\n";
- exit(0);
- }
- sub reload {
- print "reload process not implemented.\n";
- log_warning("Command 'reload' is not supported.");
- }
- sub restart {
- log_info("Restart requested...");
- stop();
- sleep(2);
- run();
- }
|