diff --git a/Netdisco/lib/App/Netdisco/Core/Arpnip.pm b/Netdisco/lib/App/Netdisco/Core/Arpnip.pm index 47252098..3480f792 100644 --- a/Netdisco/lib/App/Netdisco/Core/Arpnip.pm +++ b/Netdisco/lib/App/Netdisco/Core/Arpnip.pm @@ -3,7 +3,7 @@ package App::Netdisco::Core::Arpnip; use Dancer qw/:syntax :script/; use Dancer::Plugin::DBIC 'schema'; -use App::Netdisco::Util::SanityCheck 'check_mac'; +use App::Netdisco::Util::Node 'check_mac'; use App::Netdisco::Util::DNS ':all'; use NetAddr::IP::Lite ':lower'; use Time::HiRes 'gettimeofday'; diff --git a/Netdisco/lib/App/Netdisco/Core/Macsuck.pm b/Netdisco/lib/App/Netdisco/Core/Macsuck.pm index 1f9bb2eb..5dd94bcd 100644 --- a/Netdisco/lib/App/Netdisco/Core/Macsuck.pm +++ b/Netdisco/lib/App/Netdisco/Core/Macsuck.pm @@ -4,7 +4,7 @@ use Dancer qw/:syntax :script/; use Dancer::Plugin::DBIC 'schema'; use App::Netdisco::Util::PortMAC 'get_port_macs'; -use App::Netdisco::Util::SanityCheck 'check_mac'; +use App::Netdisco::Util::Node 'check_mac'; use App::Netdisco::Util::SNMP 'snmp_comm_reindex'; use Time::HiRes 'gettimeofday'; diff --git a/Netdisco/lib/App/Netdisco/Core/Nbtstat.pm b/Netdisco/lib/App/Netdisco/Core/Nbtstat.pm index ec0ff795..fd90c50b 100644 --- a/Netdisco/lib/App/Netdisco/Core/Nbtstat.pm +++ b/Netdisco/lib/App/Netdisco/Core/Nbtstat.pm @@ -3,7 +3,7 @@ package App::Netdisco::Core::Nbtstat; use Dancer qw/:syntax :script/; use Dancer::Plugin::DBIC 'schema'; -use App::Netdisco::Util::SanityCheck 'check_mac'; +use App::Netdisco::Util::Node 'check_mac'; use NetAddr::IP::Lite ':lower'; use Time::HiRes 'gettimeofday'; use Net::NBName; diff --git a/Netdisco/lib/App/Netdisco/Util/Device.pm b/Netdisco/lib/App/Netdisco/Util/Device.pm index d564e18c..a82b4020 100644 --- a/Netdisco/lib/App/Netdisco/Util/Device.pm +++ b/Netdisco/lib/App/Netdisco/Util/Device.pm @@ -2,17 +2,14 @@ package App::Netdisco::Util::Device; use Dancer qw/:syntax :script/; use Dancer::Plugin::DBIC 'schema'; - -use NetAddr::IP::Lite ':lower'; -use App::Netdisco::Util::DNS 'hostname_from_ip'; +use App::Netdisco::Util::Permission 'check_acl'; use base 'Exporter'; our @EXPORT = (); our @EXPORT_OK = qw/ get_device - check_acl - check_no - check_only + check_device_no + check_device_only is_discoverable is_arpnipable is_macsuckable @@ -60,87 +57,7 @@ sub get_device { ->find_or_new({ip => $ip}); } -=head2 check_acl( $ip, \@config ) - -Given the IP address of a device, returns true if any of the items in C<< -\@config >> matches that device, otherwise returns false. - -Normally you use C and C, passing the name of the -configuration setting to load. This helper instead requires not the name of -the setting, but its value. - -=cut - -sub check_acl { - my ($ip, $config) = @_; - my $device = get_device($ip) or return 0; - my $addr = NetAddr::IP::Lite->new($device->ip); - - foreach my $item (@$config) { - if (ref qr// eq ref $item) { - my $name = hostname_from_ip($addr->addr) or next; - return 1 if $name =~ $item; - next; - } - - if ($item =~ m/^([^:]+)\s*:\s*([^:]+)$/) { - my $prop = $1; - my $match = $2; - - # if not in storage, we can't do much with device properties - next unless $device->in_storage; - - # lazy version of vendor: and model: - if ($device->can($prop) and defined $device->$prop - and $device->$prop =~ m/^$match$/) { - return 1; - } - - next; - } - - if ($item =~ m/([a-f0-9]+)-([a-f0-9]+)$/i) { - my $first = $1; - my $last = $2; - - if ($item =~ m/:/) { - next unless $addr->bits == 128; - - $first = hex $first; - $last = hex $last; - - (my $header = $item) =~ s/:[^:]+$/:/; - foreach my $part ($first .. $last) { - my $ip = NetAddr::IP::Lite->new($header . sprintf('%x',$part) . '/128') - or next; - return 1 if $ip == $addr; - } - } - else { - next unless $addr->bits == 32; - - (my $header = $item) =~ s/\.[^.]+$/./; - foreach my $part ($first .. $last) { - my $ip = NetAddr::IP::Lite->new($header . $part . '/32') - or next; - return 1 if $ip == $addr; - } - } - - next; - } - - my $ip = NetAddr::IP::Lite->new($item) - or next; - next unless $ip->bits == $addr->bits; - - return 1 if $ip->contains($addr); - } - - return 0; -} - -=head2 check_no( $ip, $setting_name ) +=head2 check_device_no( $ip, $setting_name ) Given the IP address of a device, returns true if the configuration setting C<$setting_name> matches that device, else returns false. If the setting @@ -182,16 +99,17 @@ To match no devices we recommend an entry of "C" in the setting. =cut -sub check_no { +sub check_device_no { my ($ip, $setting_name) = @_; + my $device = get_device($ip) or return 0; my $config = setting($setting_name) || []; return 0 if not scalar @$config; - return check_acl($ip, $config); + return check_acl($device->ip, $config); } -=head2 check_only( $ip, $setting_name ) +=head2 check_device_only( $ip, $setting_name ) Given the IP address of a device, returns true if the configuration setting C<$setting_name> matches that device, else returns false. If the setting @@ -233,13 +151,14 @@ To match no devices we recommend an entry of "C" in the setting. =cut -sub check_only { +sub check_device_only { my ($ip, $setting_name) = @_; + my $device = get_device($ip) or return 0; my $config = setting($setting_name) || []; return 1 if not scalar @$config; - return check_acl($ip, $config); + return check_acl($device->ip, $config); } =head2 is_discoverable( $ip, $device_type? ) @@ -270,10 +189,10 @@ sub is_discoverable { } return _bail_msg("is_discoverable: device matched discover_no") - if check_no($device, 'discover_no'); + if check_device_no($device, 'discover_no'); return _bail_msg("is_discoverable: device failed to match discover_only") - unless check_only($device, 'discover_only'); + unless check_device_only($device, 'discover_only'); # cannot check last_discover for as yet undiscovered devices :-) return 1 if not $device->in_storage; @@ -304,10 +223,10 @@ sub is_arpnipable { my $device = get_device($ip) or return 0; return _bail_msg("is_arpnipable: device matched arpnip_no") - if check_no($device, 'arpnip_no'); + if check_device_no($device, 'arpnip_no'); return _bail_msg("is_arpnipable: device failed to match arpnip_only") - unless check_only($device, 'arpnip_only'); + unless check_device_only($device, 'arpnip_only'); return _bail_msg("is_arpnipable: cannot arpnip an undiscovered device") if not $device->in_storage; @@ -338,10 +257,10 @@ sub is_macsuckable { my $device = get_device($ip) or return 0; return _bail_msg("is_macsuckable: device matched macsuck_no") - if check_no($device, 'macsuck_no'); + if check_device_no($device, 'macsuck_no'); return _bail_msg("is_macsuckable: device failed to match macsuck_only") - unless check_only($device, 'macsuck_only'); + unless check_device_only($device, 'macsuck_only'); return _bail_msg("is_macsuckable: cannot macsuck an undiscovered device") if not $device->in_storage; diff --git a/Netdisco/lib/App/Netdisco/Util/Node.pm b/Netdisco/lib/App/Netdisco/Util/Node.pm index d64c0fb5..9555a57d 100644 --- a/Netdisco/lib/App/Netdisco/Util/Node.pm +++ b/Netdisco/lib/App/Netdisco/Util/Node.pm @@ -3,13 +3,13 @@ package App::Netdisco::Util::Node; use Dancer qw/:syntax :script/; use Dancer::Plugin::DBIC 'schema'; -use NetAddr::IP::Lite ':lower'; -use App::Netdisco::Util::DNS 'hostname_from_ip'; +use Net::MAC; +use App::Netdisco::Util::Permission 'check_acl'; use base 'Exporter'; our @EXPORT = (); our @EXPORT_OK = qw/ - check_node_acl + check_mac check_node_no check_node_only is_nbtstatable @@ -29,68 +29,95 @@ subroutines. =head1 EXPORT_OK -=head2 check_node_acl( $ip, \@config ) +=head2 check_mac( $device, $node, $port_macs? ) -Given the IP address of a node, returns true if any of the items in C<< -\@config >> matches that node, otherwise returns false. +Given a Device database object and a MAC address, perform various sanity +checks which need to be done before writing an ARP/Neighbor entry to the +database storage. -Normally you use C and C, passing the name of the -configuration setting to load. This helper instead requires not the name of -the setting, but its value. +Returns false, and might log a debug level message, if the checks fail. + +Returns a true value if these checks pass: + +=over 4 + +=item * + +MAC address is well-formed (according to common formats) + +=item * + +MAC address is not all-zero, broadcast, CLIP, VRRP or HSRP + +=back + +Optionally pass a cached set of Device port MAC addresses as the third +argument, in which case an additional check is added: + +=over 4 + +=item * + +MAC address does not belong to an interface on any known Device + +=back =cut -sub check_node_acl { - my ($ip, $config) = @_; - my $device = get_device($ip) or return 0; - my $addr = NetAddr::IP::Lite->new($device->ip); +sub check_mac { + my ($device, $node, $port_macs) = @_; + my $mac = Net::MAC->new(mac => $node, 'die' => 0, verbose => 0); + $port_macs ||= {}; - foreach my $item (@$config) { - if (ref qr// eq ref $item) { - my $name = hostname_from_ip($addr->addr) or next; - return 1 if $name =~ $item; - next; - } - - if ($item =~ m/([a-f0-9]+)-([a-f0-9]+)$/i) { - my $first = $1; - my $last = $2; - - if ($item =~ m/:/) { - next unless $addr->bits == 128; - - $first = hex $first; - $last = hex $last; - - (my $header = $item) =~ s/:[^:]+$/:/; - foreach my $part ($first .. $last) { - my $ip = NetAddr::IP::Lite->new($header . sprintf('%x',$part) . '/128') - or next; - return 1 if $ip == $addr; - } - } - else { - next unless $addr->bits == 32; - - (my $header = $item) =~ s/\.[^.]+$/./; - foreach my $part ($first .. $last) { - my $ip = NetAddr::IP::Lite->new($header . $part . '/32') - or next; - return 1 if $ip == $addr; - } - } - - next; - } - - my $ip = NetAddr::IP::Lite->new($item) - or next; - next unless $ip->bits == $addr->bits; - - return 1 if $ip->contains($addr); + # incomplete MAC addresses (BayRS frame relay DLCI, etc) + if ($mac->get_error) { + debug sprintf ' [%s] check_mac - mac [%s] malformed - skipping', + $device->ip, $node; + return 0; + } + else { + # lower case, hex, colon delimited, 8-bit groups + $node = lc $mac->as_IEEE; } - return 0; + # broadcast MAC addresses + return 0 if $node eq 'ff:ff:ff:ff:ff:ff'; + + # all-zero MAC addresses + return 0 if $node eq '00:00:00:00:00:00'; + + # CLIP + return 0 if $node eq '00:00:00:00:00:01'; + + # multicast + if ($node =~ m/^[0-9a-f](?:1|3|5|7|9|b|d|f):/) { + debug sprintf ' [%s] check_mac - multicast mac [%s] - skipping', + $device->ip, $node; + return 0; + } + + # VRRP + if (index($node, '00:00:5e:00:01:') == 0) { + debug sprintf ' [%s] check_mac - VRRP mac [%s] - skipping', + $device->ip, $node; + return 0; + } + + # HSRP + if (index($node, '00:00:0c:07:ac:') == 0) { + debug sprintf ' [%s] check_mac - HSRP mac [%s] - skipping', + $device->ip, $node; + return 0; + } + + # device's own MACs + if (exists $port_macs->{$node}) { + debug sprintf ' [%s] check_mac - mac [%s] is device port - skipping', + $device->ip, $node; + return 0; + } + + return 1; } =head2 check_node_no( $ip, $setting_name ) diff --git a/Netdisco/lib/App/Netdisco/Util/Permission.pm b/Netdisco/lib/App/Netdisco/Util/Permission.pm new file mode 100644 index 00000000..1d9c5d7b --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Util/Permission.pm @@ -0,0 +1,107 @@ +package App::Netdisco::Util::Permission; + +use Dancer qw/:syntax :script/; +use Dancer::Plugin::DBIC 'schema'; + +use Scalar::Util 'blessed'; +use NetAddr::IP::Lite ':lower'; + +use base 'Exporter'; +our @EXPORT = (); +our @EXPORT_OK = qw/check_acl/; +our %EXPORT_TAGS = (all => \@EXPORT_OK); + +=head1 NAME + +App::Netdisco::Util::Permission + +=head1 DESCRIPTION + +Helper subroutines to support parts of the Netdisco application. + +There are no default exports, however the C<:all> tag will export all +subroutines. + +=head1 EXPORT_OK + +=head2 check_acl( $ip, \@config ) + +Given an IP address, returns true if any of the items in C<< \@config >> +matches that address, otherwise returns false. + +Normally you use C and C, passing the name of the +configuration setting to load. This helper instead requires not the name of +the setting, but its value. + +=cut + +sub check_acl { + my ($thing, $config) = @_; + my $real_ip = (blessed $thing ? $thing->ip : $thing); + my $addr = NetAddr::IP::Lite->new($real_ip); + + foreach my $item (@$config) { + if (ref qr// eq ref $item) { + my $name = hostname_from_ip($addr->addr) or next; + return 1 if $name =~ $item; + next; + } + + if ($item =~ m/^([^:]+)\s*:\s*([^:]+)$/) { + my $prop = $1; + my $match = $2; + + # if not in storage, we can't do much with device properties + next unless blessed $thing and $thing->in_storage; + + # lazy version of vendor: and model: + if ($thing->can($prop) and defined $thing->$prop + and $thing->$prop =~ m/^$match$/) { + return 1; + } + + next; + } + + if ($item =~ m/([a-f0-9]+)-([a-f0-9]+)$/i) { + my $first = $1; + my $last = $2; + + if ($item =~ m/:/) { + next unless $addr->bits == 128; + + $first = hex $first; + $last = hex $last; + + (my $header = $item) =~ s/:[^:]+$/:/; + foreach my $part ($first .. $last) { + my $ip = NetAddr::IP::Lite->new($header . sprintf('%x',$part) . '/128') + or next; + return 1 if $ip == $addr; + } + } + else { + next unless $addr->bits == 32; + + (my $header = $item) =~ s/\.[^.]+$/./; + foreach my $part ($first .. $last) { + my $ip = NetAddr::IP::Lite->new($header . $part . '/32') + or next; + return 1 if $ip == $addr; + } + } + + next; + } + + my $ip = NetAddr::IP::Lite->new($item) + or next; + next unless $ip->bits == $addr->bits; + + return 1 if $ip->contains($addr); + } + + return 0; +} + +1; diff --git a/Netdisco/lib/App/Netdisco/Util/SNMP.pm b/Netdisco/lib/App/Netdisco/Util/SNMP.pm index 858a3f82..8af3f4a6 100644 --- a/Netdisco/lib/App/Netdisco/Util/SNMP.pm +++ b/Netdisco/lib/App/Netdisco/Util/SNMP.pm @@ -1,7 +1,8 @@ package App::Netdisco::Util::SNMP; use Dancer qw/:syntax :script/; -use App::Netdisco::Util::Device qw/get_device check_acl check_no/; +use App::Netdisco::Util::Device qw/get_device check_device_no/; +use App::Netdisco::Util::Permission qw/check_acl/; use SNMP::Info; use Try::Tiny; @@ -75,7 +76,7 @@ sub _snmp_connect_generic { ); # an override for bulkwalk - $snmp_args{BulkWalk} = 0 if check_no($device, 'bulkwalk_no'); + $snmp_args{BulkWalk} = 0 if check_device_no($device, 'bulkwalk_no'); # further protect against buggy Net-SNMP, and disable bulkwalk if ($snmp_args{BulkWalk} @@ -92,9 +93,9 @@ sub _snmp_connect_generic { # which SNMP versions to try and in what order my @versions = - ( check_no($device->ip, 'snmpforce_v3') ? (3) - : check_no($device->ip, 'snmpforce_v2') ? (2) - : check_no($device->ip, 'snmpforce_v1') ? (1) + ( check_device_no($device->ip, 'snmpforce_v3') ? (3) + : check_device_no($device->ip, 'snmpforce_v2') ? (2) + : check_device_no($device->ip, 'snmpforce_v1') ? (1) : (reverse (1 .. (setting('snmpver') || 3))) ); # use existing or new device class @@ -292,7 +293,7 @@ sub _build_communities { if not $stanza->{tag} and !exists $stanza->{community}; - if ($stanza->{$mode} and check_acl($device, $stanza->{only})) { + if ($stanza->{$mode} and check_acl($device->ip, $stanza->{only})) { if ($stored_tag and $stored_tag eq $stanza->{tag}) { # last known-good by tag unshift @communities, $stanza diff --git a/Netdisco/lib/App/Netdisco/Util/SanityCheck.pm b/Netdisco/lib/App/Netdisco/Util/SanityCheck.pm deleted file mode 100644 index 5319d840..00000000 --- a/Netdisco/lib/App/Netdisco/Util/SanityCheck.pm +++ /dev/null @@ -1,118 +0,0 @@ -package App::Netdisco::Util::SanityCheck; - -use Dancer qw/:syntax :script/; -use Dancer::Plugin::DBIC 'schema'; - -use App::Netdisco::Util::PortMAC ':all'; -use Net::MAC; - -use base 'Exporter'; -our @EXPORT = (); -our @EXPORT_OK = qw/ check_mac /; -our %EXPORT_TAGS = (all => \@EXPORT_OK); - -=head1 NAME - -App::Netdisco::Util::SanityCheck - -=head1 DESCRIPTION - -Helper subroutines to support parts of the Netdisco application. - -There are no default exports, however the C<:all> tag will export all -subroutines. - -=head1 EXPORT_OK - -=head2 check_mac( $device, $node, $port_macs? ) - -Given a Device database object and a MAC address, perform various sanity -checks which need to be done before writing an ARP/Neighbor entry to the -database storage. - -Returns false, and might log a debug level message, if the checks fail. - -Returns a true value if these checks pass: - -=over 4 - -=item * - -MAC address is well-formed (according to common formats) - -=item * - -MAC address is not all-zero, broadcast, CLIP, VRRP or HSRP - -=back - -Optionally pass a cached set of Device port MAC addresses as the third -argument, in which case an additional check is added: - -=over 4 - -=item * - -MAC address does not belong to an interface on any known Device - -=back - -=cut - -sub check_mac { - my ($device, $node, $port_macs) = @_; - my $mac = Net::MAC->new(mac => $node, 'die' => 0, verbose => 0); - $port_macs ||= {}; - - # incomplete MAC addresses (BayRS frame relay DLCI, etc) - if ($mac->get_error) { - debug sprintf ' [%s] check_mac - mac [%s] malformed - skipping', - $device->ip, $node; - return 0; - } - else { - # lower case, hex, colon delimited, 8-bit groups - $node = lc $mac->as_IEEE; - } - - # broadcast MAC addresses - return 0 if $node eq 'ff:ff:ff:ff:ff:ff'; - - # all-zero MAC addresses - return 0 if $node eq '00:00:00:00:00:00'; - - # CLIP - return 0 if $node eq '00:00:00:00:00:01'; - - # multicast - if ($node =~ m/^[0-9a-f](?:1|3|5|7|9|b|d|f):/) { - debug sprintf ' [%s] check_mac - multicast mac [%s] - skipping', - $device->ip, $node; - return 0; - } - - # VRRP - if (index($node, '00:00:5e:00:01:') == 0) { - debug sprintf ' [%s] check_mac - VRRP mac [%s] - skipping', - $device->ip, $node; - return 0; - } - - # HSRP - if (index($node, '00:00:0c:07:ac:') == 0) { - debug sprintf ' [%s] check_mac - HSRP mac [%s] - skipping', - $device->ip, $node; - return 0; - } - - # device's own MACs - if (exists $port_macs->{$node}) { - debug sprintf ' [%s] check_mac - mac [%s] is device port - skipping', - $device->ip, $node; - return 0; - } - - return 1; -} - -1;