#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use Proc::Daemon;
use Sys::Syslog;
$::PROGRAM = "netsnmp-pcap";
$::VERSION = "0.95";
#
# main
#
MAIN: {
run() unless caller();
}
#
# run()
# ---
sub run {
# default options
my %options = (
config => "/etc/snmp/pcap.conf",
debug => 0,
detach => 1,
);
# parse options
Getopt::Long::Configure(qw< no_auto_abbrev no_ignore_case >);
GetOptions(\%options, qw{
help|usage|h! man! version|V!
config|c=s base_oid|base-oid|B=s
debug|d:i detach|daemon|D! pidfile=s
}) or pod2usage(0);
# handle --version, --usage and --help
$options{man} and pod2usage(2);
$options{help} and pod2usage(1);
$options{version} and print "$::PROGRAM v$::VERSION\n" and exit;
# become a daemon
if ($options{detach}) {
if (Proc::Daemon->VERSION < 0.04) {
# old API
Proc::Daemon::Init();
if ($options{pidfile}) {
open my $fh, ">", $options{pidfile} or syslog(err =>
"error: can't write to pidfile '$options{pidfile}': $!")
and exit 1;
print {$fh} $$;
close $fh;
}
}
else {
# new API
Proc::Daemon::Init({ pid_file => $options{pidfile} });
}
}
# configure syslog
openlog($::PROGRAM, "ndelay,pid,nofatal,perror", "daemon");
syslog(info => "$::PROGRAM v$::VERSION starting");
# run the program
App::netsnmp_pcap->run(\%options);
}
#
# pod2usage()
# ---------
sub pod2usage {
my ($n) = @_;
require Pod::Usage;
Pod::Usage::pod2usage({ -exitval => 0, -verbose => $n, -noperldoc => 1 });
}
# --------------------------------------------------------------------
package App::netsnmp_pcap;
use strict;
use warnings;
use Net::Pcap qw< :functions >;
use NetSNMP::ASN;
use POE;
use POE::Component::NetSNMP::agent;
use POE::Component::Pcap;
use Sys::Syslog;
use constant {
BASE_OID => ".1.3.6.1.4.1.12325.1.1112",
AGENT_ALIAS => "snmp_agent",
ETHERNET_HEADER_LENGTH => 14,
};
my (%oid, %type, @fields);
#
# run()
# ---
sub run {
my ($class, $opts) = @_;
# init some global variables
my $base_oid = $opts->{base_oid} || BASE_OID;
%oid = (
pcapCount => "$base_oid.1",
pcapIndex => "$base_oid.2.1.0",
pcapDescr => "$base_oid.2.1.1",
pcapDevice => "$base_oid.2.1.2",
pcapFilter => "$base_oid.2.1.3",
pcapOctets => "$base_oid.2.1.4",
pcapPackets => "$base_oid.2.1.5",
);
%type = (
pcapIndex => ASN_INTEGER,
pcapDescr => ASN_OCTET_STR,
pcapDevice => ASN_OCTET_STR,
pcapFilter => ASN_OCTET_STR,
pcapOctets => ASN_COUNTER,
pcapPackets => ASN_COUNTER,
);
@fields = keys %type;
# parse the configuration
my %handlers;
my @monitors = parse_config($opts->{config});
# foreach monitor..
for my $monitor (@monitors) {
next unless defined $monitor;
my $id = $monitor->{pcapIndex};
my $name = "pcap_$monitor->{pcapIndex}";
# .. spawn a dedicated component for watching it
POE::Component::Pcap->spawn(
Alias => $name,
Device => $monitor->{pcapDevice},
Filter => $monitor->{pcapFilter},
Dispatch => "update_counters_for_$name",
Session => "main",
);
# .. and create the corresponding handler, in the main session,
# to do the actual counting
$handlers{"update_counters_for_$name"} = sub {
my ($heap, $args) = @_[ HEAP, ARG0 ];
my $pkt_header = $args->[0][0];
return if $pkt_header->{len} < ETHERNET_HEADER_LENGTH;
$heap->{monitors}[$id]{pcapPackets}++;
$heap->{monitors}[$id]{pcapOctets}
+= $pkt_header->{len} - ETHERNET_HEADER_LENGTH;
};
}
# spawn the AgentX component, that takes care of the communication
# with the SNMP daemon
POE::Component::NetSNMP::agent->spawn(
Alias => AGENT_ALIAS,
AgentX => 1,
AutoHandle => $base_oid,
Errback => "error_handler",
Debug => $opts->{debug} > 1 ? 1 : 0,
);
my @poe_opts;
push @poe_opts, options => { trace => 1, debug => 1, default => 1 }
if $opts->{debug} > 0;
# spawn the main session
POE::Session->create(
heap => {
monitors => \@monitors,
update_delay => 10,
},
inline_states => {
_start => sub {
my ($kernel, $heap) = @_[ KERNEL, HEAP ];
$kernel->alias_set("main");
$kernel->yield("update_tree");
for my $monitor (@{ $heap->{monitors} }) {
next unless defined $monitor;
$kernel->post("pcap_$monitor->{pcapIndex}", "run");
}
},
update_tree => \&update_tree,
error_handler => \&error_handler,
%handlers,
},
@poe_opts,
);
POE::Kernel->run;
}
#
# parse_config()
# ------------
sub parse_config {
my ($path) = @_;
my @monitors;
open my $fh, "<", $path
or syslog(err => "error: can't read $path: $!") and exit 1;
while (defined(my $line = <$fh>)) {
next if $line =~ /^#/;
if ($line =~ /^(pcap\w+)\.(\d+)\s+=\s+"([^"]*)"$/) {
$monitors[$2]{$1} = $3;
$monitors[$2]{pcapIndex} = $2;
}
}
close $fh;
return @monitors
}
#
# update_tree()
# -----------
sub update_tree {
my ($kernel, $heap) = @_[ KERNEL, HEAP ];
my $monitors = $heap->{monitors};
$kernel->post(AGENT_ALIAS, add_oid_entry =>
$oid{pcapCount}, ASN_INTEGER, $monitors->[-1]{pcapIndex});
for my $monitor (@$monitors) {
next unless defined $monitor;
for my $field (@fields) {
$kernel->post(AGENT_ALIAS, add_oid_entry =>
"$oid{$field}.$monitor->{pcapIndex}",
$type{$field}, $monitor->{$field},
);
}
}
$kernel->delay(update_tree => $heap->{update_delay});
}
#
# error_handler()
# -------------
sub error_handler {
my (@args) = @_[ARG0 .. $#_];
syslog(err => "error: @args");
}
1
__END__
=head1 NAME
netsnmp-pcap - SNMP extension which captures network traffic and reports
the number of packets captured, and the throughput
=for ExtUtils::MakeMaker
netsnmp::pcap::perl - SNMP extension which captures network traffic
and reports the number of packets captured, and the throughput
=head1 SYNOPSIS
netsnmp-pcap [--config /etc/snmp/pcap.conf] [--debug [n]]
netsnmp-pcap { --help | --man | --version }
=head1 OPTIONS
=head2 Program options
=over
=item B<-B>, B<--base-oid> I<OID>
Specify the base OID to server the table from. Default to the same as
C<bsnmpd-pcap>, .1.3.6.1.4.1.12325.1.1112
=item B<-c>, B<--config> I<path>
Specify the path to the configuration file. Default to F</etc/snmp/pcap.conf>
=item B<-d>, B<--debug> [I<level>]
Enable debug mode, i.e., traces POE events.
=item B<-D>, B<--detach>
Tell the program to detach itself from the terminal and become a daemon.
Use C<--no-detach> to prevent this.
=item B<-p>, B<--pidfile> I<path>
Specify the path to a file to write the PID of the daemon.
=back
=head2 Help options
=over
=item B<-h>, B<--help>
Print a short usage description, then exit.
=item B<--man>
Print the manual page of the program, then exit.
=item B<-V>, B<--version>
Print the program name and version, then exit.
=back
=head1 DESCRIPTION
This program is a port of B<bsnmpd-pcap>, the pcap plugin for FreeBSD's
bsnmpd, as an AgentX for Net-SNMP, written in Perl. It allows you to
measure arbitrary network traffic, in packets or octets, using the pcap(3)
library. Multiple flows of traffic can be measured by setting as many
network monitors, with different filters.
=head1 MIBS
The counters are available as a table under the same OID as bsnmpd-pcap,
but this can be changed using the C<--base-oid> option:
.1.3.6.1.4.1.12325.1.1112
The following entries are provided, where I<N> is the index:
=over
=item pcapCount(1)
the number of network monitors present
=item pcapTable(2).pcapEntry(1).pcapIndex(0).I<N>
the index of the network monitor
=item pcapTable(2).pcapEntry(1).pcapDescr(1).I<N>
a human description of the netowrk monitor (may be empty)
=item pcapTable(2).pcapEntry(1).pcapDevice(2).I<N>
the network device that traffic is being monitored on
=item pcapTable(2).pcapEntry(1).pcapFilter(3).I<N>
the pcap(3) filter used to select certain network packets for monitoring
=item pcapTable(2).pcapEntry(1).pcapOctets(4).I<N>
the number of octets seen by the monitor
=item pcapTable(2).pcapEntry(1).pcapPackets(5).I<N>
the number of packets seen by the monitor
=back
=head1 CONFIGURATION
The configuration format is the same as bsnmpd-pcap, where you define
the network monitors by setting the corresponding SNMP functions.
Here is an example which defines three network monitors:
%pcap
pcapDescr.1 = "ARP, ICMP and VRRP traffic"
pcapDevice.1 = "eth0"
pcapFilter.1 = "arp or icmp or vrrp"
pcapDescr.2 = "DNS traffic"
pcapDevice.2 = "eth0"
pcapFilter.2 = "port domain"
pcapDescr.3 = "HTTP traffic"
pcapDevice.3 = "eth0"
pcapFilter.3 = "port http or port https"
and the corresponding result from snmpwalk:
# snmpwalk -v2c -On -c public localhost .1.3.6.1.4.1.12325.1.1112
.1.3.6.1.4.1.12325.1.1112.1 = INTEGER: 3
.1.3.6.1.4.1.12325.1.1112.2.1.0.1 = INTEGER: 1
.1.3.6.1.4.1.12325.1.1112.2.1.0.2 = INTEGER: 2
.1.3.6.1.4.1.12325.1.1112.2.1.0.3 = INTEGER: 3
.1.3.6.1.4.1.12325.1.1112.2.1.1.1 = STRING: "ARP, ICMP and VRRP traffic"
.1.3.6.1.4.1.12325.1.1112.2.1.1.2 = STRING: "DNS traffic"
.1.3.6.1.4.1.12325.1.1112.2.1.1.3 = STRING: "HTTP traffic"
.1.3.6.1.4.1.12325.1.1112.2.1.2.1 = STRING: "eth0"
.1.3.6.1.4.1.12325.1.1112.2.1.2.2 = STRING: "eth0"
.1.3.6.1.4.1.12325.1.1112.2.1.2.3 = STRING: "eth0"
.1.3.6.1.4.1.12325.1.1112.2.1.3.1 = STRING: "arp or icmp or vrrp"
.1.3.6.1.4.1.12325.1.1112.2.1.3.2 = STRING: "port domain"
.1.3.6.1.4.1.12325.1.1112.2.1.3.3 = STRING: "port http or port https"
.1.3.6.1.4.1.12325.1.1112.2.1.4.1 = Counter32: 56
.1.3.6.1.4.1.12325.1.1112.2.1.4.2 = Counter32: 1347
.1.3.6.1.4.1.12325.1.1112.2.1.4.3 = Counter32: 29137
.1.3.6.1.4.1.12325.1.1112.2.1.5.1 = Counter32: 2
.1.3.6.1.4.1.12325.1.1112.2.1.5.2 = Counter32: 15
.1.3.6.1.4.1.12325.1.1112.2.1.5.3 = Counter32: 53
End of MIB
=head1 CAVEATS
Because this program is based on L<POE::Component::NetSNMP::agent>,
it suffers from the same problem, which is that when the snmpd daemon
it is connected to dies, the default POE loop will spin over the
half-closed Unix socket, eating 100% of CPU until the daemon is restarted
and the sub-agent has reconnected. A workaround is to use an alternative
event loop: POE::Loop::AnyEvent, POE::Loop::EV and POE::XS::Loop::EPoll
have been tested to not expose this problem.
To select the event loop, set the C<POE_EVENT_LOOP> environment variable
to its name: C<POE_EVENT_LOOP=POE::Loop::AnyEvent>
=head1 SEE ALSO
L<snmpd(1)>, L<bsnmpd-pcap(8)>, L<pcap(3)>, L<tcpdump(1)>
http://www.net-snmp.org/
http://thewalter.net/stef/software/bsnmp-pcap/
http://www.tcpdump.org/
=head1 AUTHOR
Sebastien Aperghis-Tramoni (sebastien@aperghis.net)
=cut