#!/usr/bin/perl # (c) Ártica ST 2014 # 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__