pandorafms/pandora_server/lib/PandoraFMS/Recon/Base.pm

1359 lines
41 KiB
Perl

#!/usr/bin/perl
# (c) Ártica ST 2014 <info@artica.es>
# Module for network topology discovery.
package PandoraFMS::Recon::Base;
use strict;
use warnings;
# Default lib dir for RPM and DEB packages
use lib '/usr/lib/perl5';
use NetAddr::IP;
use POSIX qw/ceil/;
use PandoraFMS::Recon::NmapParser;
use PandoraFMS::Recon::Util;
use Socket qw/inet_aton/;
# /dev/null
my $DEVNULL = ($^O eq 'MSWin32') ? '/Nul' : '/dev/null';
# Some useful OIDs.
our $DOT1DBASEBRIDGEADDRESS = ".1.3.6.1.2.1.17.1.1.0";
our $DOT1DBASEPORTIFINDEX = ".1.3.6.1.2.1.17.1.4.1.2";
our $DOT1DTPFDBADDRESS = ".1.3.6.1.2.1.17.4.3.1.1";
our $DOT1DTPFDBPORT = ".1.3.6.1.2.1.17.4.3.1.2";
our $IFDESC = ".1.3.6.1.2.1.2.2.1.2";
our $IFHCINOCTECTS = ".1.3.6.1.2.1.31.1.1.1.6";
our $IFHCOUTOCTECTS = ".1.3.6.1.2.1.31.1.1.1.10";
our $IFINDEX = ".1.3.6.1.2.1.2.2.1.1";
our $IFINOCTECTS = ".1.3.6.1.2.1.2.2.1.10";
our $IFOPERSTATUS = ".1.3.6.1.2.1.2.2.1.8";
our $IFOUTOCTECTS = ".1.3.6.1.2.1.2.2.1.16";
our $IPENTADDR = ".1.3.6.1.2.1.4.20.1.1";
our $IFNAME = ".1.3.6.1.2.1.31.1.1.1.1";
our $IFPHYSADDRESS = ".1.3.6.1.2.1.2.2.1.6";
our $IPNETTOMEDIAPHYSADDRESS = ".1.3.6.1.2.1.4.22.1.2";
our $IPADENTIFINDEX = ".1.3.6.1.2.1.4.20.1.2";
our $IPROUTEIFINDEX = ".1.3.6.1.2.1.4.21.1.2";
our $IPROUTENEXTHOP = ".1.3.6.1.2.1.4.21.1.7";
our $IPROUTETYPE = ".1.3.6.1.2.1.4.21.1.8";
our $PRTMARKERINDEX = ".1.3.6.1.2.1.43.10.2.1.1";
our $SYSDESCR = ".1.3.6.1.2.1.1.1";
our $SYSSERVICES = ".1.3.6.1.2.1.1.7";
our $SYSUPTIME = ".1.3.6.1.2.1.1.3";
our $VTPVLANIFINDEX = ".1.3.6.1.4.1.9.9.46.1.3.1.1.18.1";
our @ISA = ("Exporter");
our %EXPORT_TAGS = ( 'all' => [ qw( ) ] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
our @EXPORT = qw(
$DOT1DBASEBRIDGEADDRESS
$DOT1DBASEPORTIFINDEX
$DOT1DTPFDBADDRESS
$DOT1DTPFDBPORT
$IFDESC
$IFHCINOCTECTS
$IFHCOUTOCTECTS
$IFINDEX
$IFINOCTECTS
$IFOPERSTATUS
$IFOUTOCTECTS
$IPADENTIFINDEX
$IPENTADDR
$IFNAME
$IPNETTOMEDIAPHYSADDRESS
$IFPHYSADDRESS
$IPADENTIFINDEX
$IPROUTEIFINDEX
$IPROUTENEXTHOP
$IPROUTETYPE
$PRTMARKERINDEX
$SYSDESCR
$SYSSERVICES
$SYSUPTIME
);
#######################################################################
# Create a new ReconTask object.
#######################################################################
sub new {
my $class = shift;
my $self = {
# Known aliases (multiple IP addresses for the same host.
aliases => {},
# Keep our own ARP cache to connect hosts to switches/routers.
arp_cache => {},
# Working SNMP community for each device.
community_cache => {},
# Connections between devices.
connections => {},
# Devices by type.
hosts => [],
routers => [],
switches => [],
# Found interfaces.
ifaces => {},
# Found parents.
parents => {},
# Route cache.
routes => [],
default_gw => undef,
# SNMP query cache.
snmp_cache => {},
# Globally enable/disable SNMP scans.
snmp_enabled => 1,
# Switch to switch connections. Used to properly connect hosts
# that are connected to a switch wich is in turn connected to another switch,
# since the hosts will show up in the latter's switch AFT too.
switch_to_switch => {},
# Visited devices (initially empty).
visited_devices => {},
# Per device VLAN cache.
vlan_cache => {},
vlan_cache_enabled => 1, # User configuration. Globally disables the VLAN cache.
__vlan_cache_enabled__ => 0, # Internal state. Allows us to enable/disable the VLAN cache on a per SNMP query basis.
# Configuration parameters.
all_ifaces => 0,
communities => [],
icmp_checks => 2,
icmp_timeout => 2,
id_os => 0,
id_network_profile => 0,
nmap => '/usr/bin/nmap',
parent_detection => 1,
parent_recursion => 5,
os_detection => 0,
recon_timing_template => 3,
recon_ports => '',
resolve_names => 0,
snmp_checks => 2,
snmp_timeout => 2,
subnets => [],
@_,
};
# Perform some sanity checks.
die("No subnet was specified.") unless defined($self->{'subnets'});
# Disable SNMP scans if no community was given.
$self->{'snmp_enabled'} = 0 if (scalar(@{$self->{'communities'}}) == 0);
return bless($self, $class);
}
########################################################################################
# Add an address to a device.
########################################################################################
sub add_addresses($$$) {
my ($self, $device, $ip_address) = @_;
$self->{'visited_devices'}->{$device}->{'addr'}->{$ip_address} = '';
}
########################################################################################
# Add a MAC/IP address to the ARP cache.
########################################################################################
sub add_mac($$$) {
my ($self, $mac, $ip_addr) = @_;
$mac = parse_mac($mac);
$self->{'arp_cache'}->{$mac} = $ip_addr;
}
########################################################################################
# Add an interface/MAC to the interface cache.
########################################################################################
sub add_iface($$$) {
my ($self, $iface, $mac) = @_;
$iface =~ s/"//g;
$self->{'ifaces'}->{$mac} = $iface;
}
########################################################################################
# Discover connectivity from address forwarding tables.
########################################################################################
sub aft_connectivity($$) {
my ($self, $switch) = @_;
my (%mac_temp, @aft_temp);
return unless defined($self->get_community($switch));
$self->enable_vlan_cache();
# Get the address forwarding table (AFT) of each switch.
my @aft;
foreach my $mac ($self->snmp_get_value_array($switch, $DOT1DTPFDBADDRESS)) {
push(@aft, parse_mac($mac));
}
# Search for matching entries.
foreach my $aft_mac (@aft) {
# Do we know who this is?
my $host = $self->get_ip_from_mac($aft_mac);
next unless defined($host) and $host ne '';
# Get the name of the host interface if available.
my $host_if_name = $self->get_iface($aft_mac);
$host_if_name = defined($host_if_name) ? $host_if_name : 'ping';
# Get the interface associated to the port were we found the MAC address.
my $switch_if_name = $self->get_if_from_aft($switch, $aft_mac);
next unless defined ($switch_if_name) and ($switch_if_name ne '');
# Do not connect a host to a switch twice using the same interface.
# The switch is probably connected to another switch.
next if ($self->is_switch_connected($host, $host_if_name));
$self->mark_switch_connected($host, $host_if_name);
# The switch and the host are already connected.
next if ($self->are_connected($switch, $switch_if_name, $host, $host_if_name));
# Connect!
$self->mark_connected($switch, $switch_if_name, $host, $host_if_name);
$self->call('message', "Switch $switch (if $switch_if_name) is connected to host $host (if $host_if_name).", 5);
}
$self->disable_vlan_cache();
}
########################################################################################
# Return 1 if the given devices are connected to each other, 0 otherwise.
########################################################################################
sub are_connected($$$$$) {
my ($self, $dev_1, $if_1, $dev_2, $if_2) = @_;
# Check for aliases!
$dev_1 = $self->{'aliases'}->{$dev_1} if defined($self->{'aliases'}->{$dev_1});
$dev_2 = $self->{'aliases'}->{$dev_2} if defined($self->{'aliases'}->{$dev_2});
# Use ping modules when interfaces are unknown.
$if_1 = "ping" if $if_1 eq '';
$if_2 = "ping" if $if_2 eq '';
if (defined($self->{'connections'}->{"${dev_1}\t${if_1}\t${dev_2}\t${if_2}"}) ||
defined($self->{'connections'}->{"${dev_2}\t${if_2}\t${dev_1}\t${if_1}"})) {
return 1;
}
return 0;
}
########################################################################################
# Discover as much information as possible from the given device using SNMP.
########################################################################################
sub snmp_discovery($$) {
my ($self, $device) = @_;
# Have we already visited this device?
return if ($self->is_visited($device));
# Mark the device as visited.
$self->mark_visited($device);
# Are SNMP scans enabled?
if ($self->{'snmp_enabled'} == 1) {
# Try to find the MAC with an ARP request.
$self->get_mac_from_ip($device);
# Check if the device responds to SNMP.
if ($self->snmp_responds($device)) {
# Fill the VLAN cache.
$self->find_vlans($device);
# Guess the device type.
$self->guess_device_type($device);
# Find aliases for the device.
$self->find_aliases($device);
# Find interfaces for the device.
$self->find_ifaces($device);
# Try to learn more MAC addresses from the device's ARP cache.
my @output = $self->snmp_get($device, $IPNETTOMEDIAPHYSADDRESS);
foreach my $line (@output) {
next unless ($line =~ /^$IPNETTOMEDIAPHYSADDRESS.\d+.(\S+)\s+=\s+\S+:\s+(.*)$/);
my ($ip_addr, $mac_addr) = ($1, $2);
# Skip broadcast, net and local addresses.
next if ($ip_addr =~ m/\.255$|\.0$|127\.0\.0\.1$/);
$mac_addr = parse_mac($mac_addr);
$self->add_mac($mac_addr, $ip_addr);
$self->call('message', "Found MAC $mac_addr for host $ip_addr in the ARP cache of host $device.", 5);
}
}
}
# Create an agent for the device and add it to the list of known hosts.
push(@{$self->{'hosts'}}, $device);
$self->call('create_agent', $device);
}
#######################################################################
# Try to call the given function on the given object.
#######################################################################
sub call {
my $self = shift;
my $func = shift;
my @params = @_;
if ($self->can($func)) {
$self->$func(@params);
}
}
########################################################################################
# Disable the VLAN cache.
########################################################################################
sub disable_vlan_cache($$) {
my ($self, $device) = @_;
$self->{'__vlan_cache_enabled__'} = 0;
}
########################################################################################
# Enable the VLAN cache.
########################################################################################
sub enable_vlan_cache($$) {
my ($self, $device) = @_;
return if ($self->{'vlan_cache_enabled'} == 0);
$self->{'__vlan_cache_enabled__'} = 1;
}
##########################################################################
# Connect the given hosts to its gateway.
##########################################################################
sub gateway_connectivity($$) {
my ($self, $host) = @_;
my $gw = $self->get_gateway($host);
return unless defined($gw);
# Check for aliases!
$host = $self->{'aliases'}->{$host} if defined($self->{'aliases'}->{$host});
$gw = $self->{'aliases'}->{$gw} if defined($self->{'aliases'}->{$gw});
# Same host, different IP addresses.
return if ($host eq $gw);
$self->call('message', "Host $host is reached via gateway $gw.", 5);
$self->mark_connected($gw, '', $host, '');
}
########################################################################################
# Find IP address aliases for the given device.
########################################################################################
sub find_aliases($$) {
my ($self, $device) = @_;
# Get ARP cache.
my @ip_addresses = $self->snmp_get_value_array($device, $IPENTADDR);
foreach my $ip_address (@ip_addresses) {
# Skip broadcast and localhost addresses.
next if ($ip_address =~ m/\.255$|\.0$|127\.0\.0\.1$/);
# Sometimes we find the same IP address we had.
next if ($ip_address eq $device);
$self->add_addresses($device, $ip_address);
# Try to find the MAC with an ARP request.
$self->get_mac_from_ip($ip_address);
$self->call('message', "Found address $ip_address for host $device.", 5);
# Is this address an alias itself?
$device = $self->{'aliases'}->{$device} if defined($self->{'aliases'}->{$device});
next if ($ip_address eq $device);
# Link the two addresses.
$self->{'aliases'}->{$ip_address} = $device;
}
}
########################################################################################
# Find all the interfaces for the given host.
########################################################################################
sub find_ifaces($$) {
my ($self, $device) = @_;
# Does it respond to SNMP?
my $community = $self->get_community($device);
return unless defined($community);
my @output = $self->snmp_get_value_array($device, $PandoraFMS::Recon::Base::IFINDEX);
foreach my $if_index (@output) {
next unless ($if_index =~ /^[0-9]+$/);
# Get the MAC.
my $mac = $self->get_if_mac($device, $if_index);
next unless (defined($mac) && $mac ne '');
# Save it.
$self->add_mac($mac, $device);
# Get the name of the network interface.
my $if_name = $self->snmp_get_value($device, "$PandoraFMS::Recon::Base::IFNAME.$if_index");
next unless defined($if_name);
# Save it.
$self->add_iface($if_name, $mac);
$self->call('message', "Found interface $if_name MAC $mac for host $device.", 5);
}
}
########################################################################################
# Find the device's VLANs and fill the VLAN cache.
########################################################################################
sub find_vlans ($$) {
my ($self, $device) = @_;
my %vlan_hash;
foreach my $vlan ($self->snmp_get_value_array($device, $VTPVLANIFINDEX)) {
next if $vlan eq '0';
$vlan_hash{$vlan} = 1;
}
my @vlans = keys(%vlan_hash);
$self->{'vlan_cache'}->{$device} = [];
push(@{$self->{'vlan_cache'}->{$device}}, @vlans) if (scalar(@vlans) > 0);
}
########################################################################################
# Return the addresses of the given device as an array.
########################################################################################
sub get_addresses($$) {
my ($self, $device) = @_;
if (defined($self->{'visited_devices'}->{$device})) {
return keys(%{$self->{'visited_devices'}->{$device}->{'addr'}});
}
# By default return the given address.
return ($device);
}
########################################################################################
# Return a device structure from an IP address.
########################################################################################
sub get_device($$) {
my ($self, $addr) = @_;
if (defined($self->{'visited_devices'}->{$addr})) {
return $self->{'visited_devices'}->{$addr};
}
return undef;
}
########################################################################################
# Get the SNMP community of the given device. Returns undef if no community was found.
########################################################################################
sub get_community($$) {
my ($self, $device) = @_;
if (defined($self->{'community_cache'}->{$device})) {
return $self->{'community_cache'}->{$device};
}
return undef;
}
########################################################################################
# Return the connection hash.
########################################################################################
sub get_connections($) {
my ($self) = @_;
return $self->{'connections'};
}
########################################################################################
# Return the parent relationship hash.
########################################################################################
sub get_parents($) {
my ($self) = @_;
return $self->{'parents'};
}
########################################################################################
# Get the type of the given device.
########################################################################################
sub get_device_type($$) {
my ($self, $device) = @_;
if (defined($self->{'visited_devices'}->{$device})) {
return $self->{'visited_devices'}->{$device}->{'type'};
}
# Assume 'host' by default.
return 'host';
}
########################################################################################
# Return all known hosts that are not switches or routers.
########################################################################################
sub get_hosts($) {
my ($self) = @_;
return $self->{'hosts'};
}
########################################################################################
# Add an interface/MAC to the interface cache.
########################################################################################
sub get_iface($$) {
my ($self, $mac) = @_;
return undef unless defined($self->{'ifaces'}->{$mac});
return $self->{'ifaces'}->{$mac};
}
########################################################################################
# Get an interface name from an AFT entry. Returns undef on error.
########################################################################################
sub get_if_from_aft($$$) {
my ($self, $switch, $mac) = @_;
# Get the port associated to the MAC.
my $port = $self->snmp_get_value($switch, "$DOT1DTPFDBPORT." . mac_to_dec($mac));
return '' unless defined($port);
# Get the interface index associated to the port.
my $if_index = $self->snmp_get_value($switch, "$DOT1DBASEPORTIFINDEX.$port");
return '' unless defined($if_index);
# Get the interface name.
my $if_name = $self->snmp_get_value($switch, "$IFNAME.$if_index");
return "if$if_index" unless defined($if_name);
$if_name =~ s/"//g;
return $if_name;
}
########################################################################################
# Get an interface name from an IP address.
########################################################################################
sub get_if_from_ip($$$) {
my ($self, $device, $ip_addr) = @_;
# Get the port associated to the IP address.
my $if_index = $self->snmp_get_value($device, "$IPROUTEIFINDEX.$ip_addr");
return '' unless defined ($if_index);
# Get the name of the interface associated to the port.
my $if_name = $self->snmp_get_value($device, "$IFNAME.$if_index");
return '' unless defined ($if_name);
$if_name =~ s/"//g;
return $if_name;
}
########################################################################################
# Get an interface name from a MAC address.
########################################################################################
sub get_if_from_mac($$$) {
my ($self, $device, $mac) = @_;
# Get the port associated to the IP address.
my @output = $self->snmp_get($device, $IFPHYSADDRESS);
foreach my $line (@output) {
chomp($line);
next unless $line =~ /^IFPHYSADDRESS.(\S+)\s+=\s+\S+:\s+(.*)$/;
my ($if_index, $if_mac) = ($1, $2);
# Make sure the MAC addresses match.
next unless (mac_matches($mac, $if_mac) == 1);
# Pupulate the ARP cache.
$self->add_mac($mac, $device);
# Get the name of the interface associated to the port.
my $if_name = $self->snmp_get_value($device, "$IFNAME.$if_index");
return '' unless defined ($if_name);
$if_name =~ s/"//g;
return $if_name;
}
return '';
}
########################################################################################
# Get an interface name from a port number. Returns '' on error.
########################################################################################
sub get_if_from_port($$$) {
my ($self, $switch, $port) = @_;
# Get the interface index associated to the port.
my $if_index = $self->snmp_get_value($switch, "$DOT1DBASEPORTIFINDEX.$port");
return '' unless defined($if_index);
# Get the interface name.
my $if_name = $self->snmp_get_value($switch, "$IFNAME.$if_index");
return "if$if_index" unless defined($if_name);
$if_name =~ s/"//g;
return $if_name;
}
########################################################################################
# Returns the IP address of the given interface (by index).
########################################################################################
sub get_if_ip($$$) {
my ($self, $device, $if_index) = @_;
my @output = $self->snmp_get($device, $IPADENTIFINDEX);
foreach my $line (@output) {
chomp ($line);
return $1 if ($line =~ m/^IPADENTIFINDEX.(\S+)\s+=\s+\S+:\s+$if_index$/);
}
return '';
}
########################################################################################
# Returns the MAC address of the given interface (by index).
########################################################################################
sub get_if_mac($$$) {
my ($self, $device, $if_index) = @_;
my $mac = $self->snmp_get_value($device, "$IFPHYSADDRESS.$if_index");
return '' unless defined($mac);
# Clean-up the MAC address.
$mac = parse_mac($mac);
return $mac;
}
########################################################################################
# Get an IP address from the ARP cache given the MAC address.
########################################################################################
sub get_ip_from_mac($$) {
my ($self, $mac_addr) = @_;
if (defined($self->{'arp_cache'}->{$mac_addr})) {
return $self->{'arp_cache'}->{$mac_addr};
}
return undef;
}
########################################################################################
# Attemtps to find
########################################################################################
sub get_mac_from_ip($$) {
my ($self, $host) = @_;
my $mac = undef;
eval {
$mac = `arping -c 1 -r $host 2>$DEVNULL`;
$mac = undef unless ($? == 0);
};
return unless defined($mac);
# Clean-up the MAC address.
chomp($mac);
$mac = parse_mac($mac);
$self->add_mac($mac, $host);
$self->call('message', "Found MAC $mac for host $host in the local ARP cache.", 5);
}
########################################################################################
# Get a port number from an AFT entry. Returns undef on error.
########################################################################################
sub get_port_from_aft($$$) {
my ($self, $switch, $mac) = @_;
# Get the port associated to the MAC.
my $port = $self->snmp_get_value($switch, "$DOT1DTPFDBPORT." . mac_to_dec($mac));
return '' unless defined($port);
return $port;
}
########################################################################################
# Fill the route cache.
########################################################################################
sub get_routes($) {
my ($self) = @_;
# Empty the current route cache.
$self->{'routes'} = [];
# Parse route's output.
my @output = `route -n 2>/dev/null`;
foreach my $line (@output) {
chomp($line);
if ($line =~ /^0\.0\.0\.0\s+(\d+\.\d+\.\d+\.\d+).*/) {
$self->{'default_gw'} = $1;
} elsif ($line =~ /^(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+).*/) {
push(@{$self->{'routes'}}, { dest => $1, gw => $2, mask => $3 });
}
}
# Replace 0.0.0.0 with the default gateway's IP.
return unless defined ($self->{'default_gw'});
foreach my $route (@{$self->{'routes'}}) {
$route->{gw} = $self->{'default_gw'} if ($route->{'gw'} eq '0.0.0.0');
}
}
########################################################################################
# Get the gateway to reach the given host.
########################################################################################
sub get_gateway($) {
my ($self, $host) = @_;
# Look for a specific route to the given host.
foreach my $route (@{$self->{'routes'}}) {
if (subnet_matches($host, $route->{'dest'}, $route->{'mask'})) {
return $route->{'gw'};
}
}
# Return the default gateway.
return $self->{'default_gw'} if defined($self->{'default_gw'});
# Ops!
return undef;
}
########################################################################################
# Return a pointer to an array containing configured subnets.
########################################################################################
sub get_subnets($) {
my ($self) = @_;
return $self->{'subnets'};
}
########################################################################################
# Get an array of all the visited devices.
# NOTE: This functions returns the whole device structures, not just address
# like get_hosts, get_switches, get_routers and get_all_devices.
########################################################################################
sub get_visited_devices($) {
my ($self) = @_;
return $self->{'visited_devices'};
}
########################################################################################
# Returns an array of found VLAN IDs.
########################################################################################
sub get_vlans($$) {
my ($self, $device) = @_;
# Is the VLAN cache disabled?
return () unless ($self->{'__vlan_cache_enabled__'} == 1);
return () unless defined($self->{'vlan_cache'}->{$device});
return @{$self->{'vlan_cache'}->{$device}};
}
########################################################################################
# Guess the type of the given device.
########################################################################################
sub guess_device_type($$) {
my ($self, $device) = @_;
# Get the value of sysServices.
my $services = $self->snmp_get_value($device, "$SYSSERVICES.0");
return unless defined($services);
# Check the individual bits.
my @service_bits = split('', unpack('b8', pack('C', $services)));
# Check for layer 2 connectivity support.
my $bridge_mib = $self->snmp_get_value($device, $DOT1DBASEBRIDGEADDRESS);
# L2?
my $device_type;
if ($service_bits[1] == 1) {
# L3?
if ($service_bits[2] == 1) {
# Bridge MIB?
if (defined($bridge_mib)) {
$device_type = 'switch';
} else {
# L7?
if ($service_bits[6] == 1) {
$device_type = 'host';
} else {
$device_type = 'router';
}
}
}
else {
# Bridge MIB?
if (defined($bridge_mib)) {
$device_type = 'switch';
} else {
$device_type = 'host';
}
}
}
else {
# L3?
if ($service_bits[2] == 1) {
# L4?
if ($service_bits[3] == 1) {
$device_type = 'switch';
} else {
# L7?
if ($service_bits[6] == 1) {
$device_type = 'host';
} else {
$device_type = 'router';
}
}
}
else {
# Printer MIB?
my $printer_mib = $self->snmp_get_value($device, $PRTMARKERINDEX);
if (defined($printer_mib)) {
$device_type = 'printer';
} else {
$device_type = 'host';
}
}
}
# Set the type of the device.
$self->set_device_type($device, $device_type);
}
########################################################################################
# Return 1 if the given device has a parent.
########################################################################################
sub has_parent($$) {
my ($self, $device) = @_;
# Check for aliases!
$device = $self->{'aliases'}->{$device} if defined($self->{'aliases'}->{$device});
return 1 if (defined($self->{'parents'}->{$device}));
return 0;
}
########################################################################################
# Returns 1 if the device belongs to one of the scanned subnets.
########################################################################################
sub in_subnet($$) {
my ($self, $device) = @_;
$device = ip_to_long($device);
# No subnets specified.
return 1 if (scalar(@{$self->{'subnets'}}) <= 0);
foreach my $subnet (@{$self->{'subnets'}}) {
if (subnet_matches($device, $subnet)) {
return 1;
}
}
return 0;
}
##########################################################################
# Check for switches that are connected to other switches/routers and show
# up in a switch/router's port.
##########################################################################
sub is_switch_connected($$$) {
my ($self, $device, $iface) = @_;
# Check for aliases!
$device = $self->{'aliases'}->{$device} if defined($self->{'aliases'}->{$device});
return 1 if defined ($self->{'switch_to_switch'}->{"${device}\t${iface}"});
return 0;
}
########################################################################################
# Returns 1 if the given device has already been visited, 0 otherwise.
########################################################################################
sub is_visited($$) {
my ($self, $device) = @_;
# Check for aliases!
$device = $self->{'aliases'}->{$device} if defined($self->{'aliases'}->{$device});
if (defined($self->{'visited_devices'}->{$device})) {
return 1;
}
return 0;
}
########################################################################################
# Mark the given devices as connected to each other on the given interfaces.
########################################################################################
sub mark_connected($$;$$$) {
my ($self, $parent, $parent_if, $child, $child_if) = @_;
# Check for aliases!
$parent = $self->{'aliases'}->{$parent} if defined($self->{'aliases'}->{$parent});
$child = $self->{'aliases'}->{$child} if defined($self->{'aliases'}->{$child});
# Use ping modules when interfaces are unknown.
$parent_if = "ping" if $parent_if eq '';
$child_if = "ping" if $child_if eq '';
# Do not connect devices using ping modules. A parent-child relationship is enough.
if ($parent_if ne "ping" || $child_if ne "ping") {
$self->{'connections'}->{"${parent}\t${parent_if}\t${child}\t${child_if}"} = 1;
$self->call('connect_agents', $parent, $parent_if, $child, $child_if);
}
# Prevent parent-child loops.
if (!defined($self->{'parents'}->{$parent}) ||
$self->{'parents'}->{$parent} ne $child) {
# A parent-child relationship is always created to help complete the map with
# layer 3 information.
$self->{'parents'}->{$child} = $parent;
$self->call('set_parent', $child, $parent);
}
}
########################################################################################
# Mark the given switch as having a connection on the given interface.
########################################################################################
sub mark_switch_connected($$$) {
my ($self, $device, $iface) = @_;
# Check for aliases!
$device = $self->{'aliases'}->{$device} if defined($self->{'aliases'}->{$device});
$self->{'switch_to_switch'}->{"${device}\t${iface}"} = 1;
}
########################################################################################
# Mark the given device as visited.
########################################################################################
sub mark_visited($$) {
my ($self, $device) = @_;
$self->{'visited_devices'}->{$device} = { 'addr' => { $device => '' },
'type' => 'host' };
}
########################################################################################
# Looks for a working SNMP community for the given device. Returns 1 if one is
# found, 0 otherwise. Updates the SNMP community cache.
########################################################################################
sub snmp_responds($$) {
my ($self, $device) = @_;
# We already have a working SNMP community for this device.
return 1 if (defined($self->get_community($device)));
foreach my $community (@{$self->{'communities'}}) {
# Clean blanks.
$community =~ s/\s+//g;
`snmpwalk -M/dev/null -r$self->{'snmp_checks'} -t$self->{'snmp_timeout'} -v1 -On -Oe -c $community $device .0 2>/dev/null`;
if ($? == 0) {
$self->set_community($device, $community);
return 1;
}
}
return 0;
}
##############################################################################
# Ping the given host. Returns 1 if the host is alive, 0 otherwise.
##############################################################################
sub ping ($$$) {
my ($self, $host) = @_;
my ($timeout, $retries, $packets) = (
$self->{'icmp_timeout'},
$self->{'icmp_checks'},
1,
);
# Windows
if (($^O eq "MSWin32") || ($^O eq "MSWin32-x64") || ($^O eq "cygwin")){
$timeout *= 1000; # Convert the timeout to milliseconds.
for (my $i = 0; $i < $retries; $i++) {
my $output = `ping -n $packets -w $timeout $host`;
return 1 if ($output =~ /TTL/);
}
return 0;
}
# Solaris
if ($^O eq "solaris"){
my $ping_command = $host =~ /\d+:|:\d+/ ? "ping -A inet6" : "ping";
for (my $i = 0; $i < $retries; $i++) {
# Note: There is no timeout option.
`$ping_command -s -n $host 56 $packets >/dev/null 2>&1`;
return 1 if ($? == 0);
}
return 0;
}
# FreeBSD
if ($^O eq "freebsd"){
my $ping_command = $host =~ /\d+:|:\d+/ ? "ping6" : "ping -t $timeout";
for (my $i = 0; $i < $retries; $i++) {
# Note: There is no timeout option for ping6.
`$ping_command -q -n -c $packets $host >/dev/null 2>&1`;
return 1 if ($? == 0);
}
return 0;
}
# NetBSD
if ($^O eq "netbsd"){
my $ping_command = $host =~ /\d+:|:\d+/ ? "ping6" : "ping -w $timeout";
for (my $i = 0; $i < $retries; $i++) {
# Note: There is no timeout option for ping6.
`$ping_command -q -n -c $packets $host >/dev/null 2>&1`;
if ($? == 0) {
return 1;
}
}
return 0;
}
# Assume Linux by default.
my $ping_command = $host =~ /\d+:|:\d+/ ? "ping6" : "ping";
for (my $i = 0; $i < $retries; $i++) {
`$ping_command -q -W $timeout -n -c $packets $host >/dev/null 2>&1`;
return 1 if ($? == 0);
}
return 0;
}
##########################################################################
# Scan the given subnet.
##########################################################################
sub scan_subnet($) {
my ($self) = @_;
my $progress = 1;
my @subnets = @{$self->get_subnets()};
foreach my $subnet (@subnets) {
# Clean blanks.
$subnet =~ s/\s+//g;
my $net_addr = new NetAddr::IP ($subnet);
if (!defined($net_addr)) {
$self->call('message', "Invalid network: $subnet", 3);
next;
}
# Save the network and broadcast addresses.
my $network = $net_addr->network();
my $broadcast = $net_addr->broadcast();
# fping scan.
if (-x $self->{'fping'} && $net_addr->num() > 1) {
$self->call('message', "Calling fping...", 5);
my @hosts = `$self->{'fping'} -ga "$subnet" 2>DEVNULL`;
next if (scalar(@hosts) == 0);
my $step = 50.0 / scalar(@subnets) / scalar(@hosts); # The first 50% of the recon task approx.
foreach my $line (@hosts) {
chomp($line);
my @temp = split(/ /, $line);
next if (scalar(@temp) != 1); # Junk is shown for broadcast addresses.
my $host = $temp[0];
# Skip network and broadcast addresses.
next if ($host eq $network->addr() || $host eq $broadcast->addr());
$self->call('message', "Scanning host: $host", 5);
$self->call('update_progress', ceil($progress));
$progress += $step;
$self->snmp_discovery($host);
}
}
# ping scan.
else {
my @hosts = map { (split('/', $_))[0] } $net_addr->hostenum;
next if (scalar(@hosts) == 0);
my $step = 50.0 / scalar(@subnets) / scalar(@hosts); # The first 50% of the recon task approx.
foreach my $host (@hosts) {
$self->call('message', "Scanning host: $host", 5);
$self->call('update_progress', ceil($progress));
$progress += $step;
# Check if the host is up.
next if ($self->ping($host) == 0);
$self->snmp_discovery($host);
}
}
}
}
##########################################################################
# Perform a network scan.
##########################################################################
sub scan($) {
my ($self) = @_;
my ($progress, $step);
# 1%
$self->call('update_progress', 1);
# Find devices.
$self->call('message', "[1/5] Scanning the network...", 3);
$self->scan_subnet();
# Get a list of found hosts.
my @hosts = @{$self->get_hosts()};
if (scalar(@hosts) > 0 && $self->{'parent_detection'} == 1) {
# Delete previous connections.
$self->call('delete_connections');
# Connectivity from address forwarding tables.
$self->call('message', "[1/4] Finding address forwarding table connectivity...", 3);
($progress, $step) = (50, 20.0 / scalar(@hosts)); # From 50% to 70%.
for (my $i = 0; defined($hosts[$i]); $i++) {
$self->call('update_progress', $progress);
$progress += $step;
$self->aft_connectivity($hosts[$i]);
}
# Connect hosts that are still unconnected using traceroute.
$self->call('message', "[3/4] Finding traceroute connectivity.", 3);
($progress, $step) = (70, 20.0 / scalar(@hosts)); # From 70% to 90%.
foreach my $host (@hosts) {
$self->call('update_progress', $progress);
$progress += $step;
next if ($self->has_parent($host));
$self->traceroute_connectivity($host);
}
# Connect hosts that are still unconnected using known gateways.
$self->call('message', "[4/4] Finding host to gateway connectivity.", 3);
($progress, $step) = (90, 10.0 / scalar(@hosts)); # From 70% to 90%.
$self->get_routes(); # Update the route cache.
foreach my $host (@hosts) {
$self->call('update_progress', $progress);
$progress += $step;
next if ($self->has_parent($host));
$self->gateway_connectivity($host);
}
}
# Done!
$self->call('update_progress', -1);
# Print debug information on found devices.
$self->call('message', "[Summary]", 3);
foreach my $host (@hosts) {
my $device = $self->get_device($host);
next unless defined($device);
# Print device information.
my $dev_info = "Device: " . $device->{'type'} . " (";
foreach my $ip_address ($self->get_addresses($host)) {
$dev_info .= "$ip_address,";
}
chop($dev_info);
$dev_info .= ')';
$self->call('message', $dev_info, 3);
}
}
########################################################################################
# Set an SNMP community for the given device.
########################################################################################
sub set_community($$$) {
my ($self, $device, $community) = @_;
$self->{'community_cache'}->{$device} = $community;
}
########################################################################################
# Set the type of the given device.
########################################################################################
sub set_device_type($$$) {
my ($self, $device, $type) = @_;
$self->{'visited_devices'}->{$device}->{'type'} = $type;
}
########################################################################################
# Performs an SNMP WALK and returns the response as an array.
########################################################################################
sub snmp_get($$$) {
my ($self, $device, $oid) = @_;
my @output;
my $community = $self->get_community($device);
return () unless defined ($community);
# Check the SNMP query cache first.
if (defined($self->{'snmp_cache'}->{"${device}_${oid}"})) {
return @{$self->{'snmp_cache'}->{"${device}_${oid}"}};
}
# Check VLANS.
my @vlans = $self->get_vlans($device);
if (scalar(@vlans) == 0) {
@output = `snmpwalk -M/dev/null -r$self->{'snmp_checks'} -t$self->{'snmp_timeout'} -v1 -On -Oe -c $community $device $oid 2>/dev/null`;
}
else {
# Handle duplicate lines.
my %output_hash;
foreach my $vlan (@vlans) {
foreach my $line (`snmpwalk -M/dev/null -r$self->{'snmp_checks'} -t$self->{'snmp_timeout'} -v1 -On -Oe -c $community\@$vlan $device $oid 2>/dev/null`) {
$output_hash{$line} = 1;
}
}
push(@output, keys(%output_hash));
}
# Update the SNMP query cache.
$self->{'snmp_cache'}->{"${device}_${oid}"} = [@output];
return @output;
}
########################################################################################
# Performs an SNMP WALK and returns the value of the given OID. Returns undef
# on error.
########################################################################################
sub snmp_get_value($$$) {
my ($self, $device, $oid) = @_;
my @output = $self->snmp_get($device, $oid);
foreach my $line (@output) {
chomp ($line);
return $1 if ($line =~ /^$oid\s+=\s+\S+:\s+(.*)$/);
}
return undef;
}
########################################################################################
# Performs an SNMP WALK and returns an array of values.
########################################################################################
sub snmp_get_value_array($$$) {
my ($self, $device, $oid) = @_;
my @values;
my @output = $self->snmp_get($device, $oid);
foreach my $line (@output) {
chomp ($line);
push(@values, $1) if ($line =~ /^$oid\S*\s+=\s+\S+:\s+(.*)$/);
}
return @values;
}
########################################################################################
# Performs an SNMP WALK and returns a hash of values.
########################################################################################
sub snmp_get_value_hash($$$) {
my ($self, $device, $oid) = @_;
my %values;
my @output = $self->snmp_get_value_array($device, $oid);
foreach my $line (@output) {
$values{$line} = '';
}
return %values;
}
##########################################################################
# Connect the given host to its parent using traceroute.
##########################################################################
sub traceroute_connectivity($$) {
my ($self, $host) = @_;
# Perform a traceroute.
my $nmap_args = '-nsP -PE --traceroute --max-retries '.$self->{'icmp_checks'}.' --host-timeout '.$self->{'icmp_timeout'}.'s -T'.$self->{'recon_timing_template'};
my $np = PandoraFMS::Recon::NmapParser->new();
eval {
$np->parsescan($self->{'nmap'}, $nmap_args, ($host));
};
return if ($@);
# Get hops to the host.
my ($h) = $np->all_hosts ();
return unless defined ($h);
my @hops = $h->all_trace_hops ();
# Skip the target host.
pop(@hops);
# Reverse the host order (closest hosts first).
@hops = reverse(@hops);
# Look for parents.
my $device = $host;
for (my $i = 0; $i < $self->{'parent_recursion'}; $i++) {
next unless defined($hops[$i]);
my $parent = $hops[$i]->ipaddr();
# Create an agent for the parent.
$self->call('create_agent', $parent);
$self->call('message', "Host $device is one hop away from host $parent.", 5);
$self->mark_connected($parent, '', $device, '');
# Move on to the next hop.
$device = $parent;
}
}
1;
__END__