File Coverage

blib/lib/Net/SNMPTrapd.pm
Criterion Covered Total %
statement 31 250 12.4
branch 0 108 0.0
condition 0 39 0.0
subroutine 11 34 32.3
pod 21 21 100.0
total 63 452 13.9


line stmt bran cond sub pod time code
1             package Net::SNMPTrapd;
2              
3             ########################################################
4             # AUTHOR = Michael Vincent
5             # www.VinsWorld.com
6             ########################################################
7              
8 1     1   31617 use strict;
  1         2  
  1         44  
9 1     1   7 use warnings;
  1         2  
  1         50  
10 1     1   493 use version;
  1         1955  
  1         6  
11 1     1   102 BEGIN { *Version:: = \*version:: }
12             # version module conflicts with 'sub version()' below.
13             # poor man's Package::Alias to avoid additional dependency
14             # http://www.perlmonks.org/?node_id=823772
15 1     1   676 use Convert::ASN1;
  1         48852  
  1         74  
16 1     1   10 use Socket qw(inet_ntoa AF_INET IPPROTO_TCP);
  1         2  
  1         479  
17              
18             my $AF_INET6 = eval { Socket::AF_INET6() };
19             my $NI_NUMERICHOST = eval { Socket::NI_NUMERICHOST() };
20              
21             our $VERSION = '0.17';
22             our @ISA;
23              
24             my $HAVE_IO_Socket_IP = 0;
25 1     1   1034 eval "use IO::Socket::IP -register";
  1         47547  
  1         21  
26             if(!$@) {
27             $HAVE_IO_Socket_IP = 1;
28             push @ISA, "IO::Socket::IP"
29             } else {
30             require IO::Socket::INET;
31             push @ISA, "IO::Socket::INET";
32             }
33              
34             ########################################################
35             # Start Variables
36             ########################################################
37 1     1   5 use constant SNMPTRAPD_DEFAULT_PORT => 162;
  1         1  
  1         82  
38 1     1   4 use constant SNMPTRAPD_RFC_SIZE => 484; # RFC limit
  1         2  
  1         44  
39 1     1   4 use constant SNMPTRAPD_REC_SIZE => 1472; # Recommended size
  1         2  
  1         66  
40 1     1   6 use constant SNMPTRAPD_MAX_SIZE => 65467; # Actual limit (65535 - IP/UDP)
  1         2  
  1         3414  
41              
42             my @TRAPTYPES = qw(COLDSTART WARMSTART LINKDOWN LINKUP AUTHFAIL EGPNEIGHBORLOSS ENTERPRISESPECIFIC);
43             my @PDUTYPES = qw(GetRequest GetNextRequest Response SetRequest Trap GetBulkRequest InformRequest SNMPv2-Trap Report);
44             our $LASTERROR;
45              
46             my $asn = Convert::ASN1->new;
47             $asn->prepare("
48             PDU ::= SEQUENCE {
49             version INTEGER,
50             community STRING,
51             pdu_type PDUs
52             }
53             PDUs ::= CHOICE {
54             response Response_PDU,
55             trap Trap_PDU,
56             inform_request InformRequest_PDU,
57             snmpv2_trap SNMPv2_Trap_PDU
58             }
59             Response_PDU ::= [2] IMPLICIT PDUv2
60             Trap_PDU ::= [4] IMPLICIT PDUv1
61             InformRequest_PDU ::= [6] IMPLICIT PDUv2
62             SNMPv2_Trap_PDU ::= [7] IMPLICIT PDUv2
63              
64             IPAddress ::= [APPLICATION 0] STRING
65             Counter32 ::= [APPLICATION 1] INTEGER
66             Guage32 ::= [APPLICATION 2] INTEGER
67             TimeTicks ::= [APPLICATION 3] INTEGER
68             Opaque ::= [APPLICATION 4] STRING
69             Counter64 ::= [APPLICATION 6] INTEGER
70              
71             PDUv1 ::= SEQUENCE {
72             ent_oid OBJECT IDENTIFIER,
73             agent_addr IPAddress,
74             generic_trap INTEGER,
75             specific_trap INTEGER,
76             timeticks TimeTicks,
77             varbindlist VARBINDS
78             }
79             PDUv2 ::= SEQUENCE {
80             request_id INTEGER,
81             error_status INTEGER,
82             error_index INTEGER,
83             varbindlist VARBINDS
84             }
85             VARBINDS ::= SEQUENCE OF SEQUENCE {
86             oid OBJECT IDENTIFIER,
87             value CHOICE {
88             integer INTEGER,
89             string STRING,
90             oid OBJECT IDENTIFIER,
91             ipaddr IPAddress,
92             counter32 Counter32,
93             guage32 Guage32,
94             timeticks TimeTicks,
95             opaque Opaque,
96             counter64 Counter64,
97             null NULL
98             }
99             }
100             ");
101             my $snmpasn = $asn->find('PDU');
102             ########################################################
103             # End Variables
104             ########################################################
105              
106             ########################################################
107             # Start Public Module
108             ########################################################
109              
110             sub new {
111 0     0 1   my $self = shift;
112 0   0       my $class = ref($self) || $self;
113              
114             # Default parameters
115 0           my %params = (
116             'Proto' => 'udp',
117             'LocalPort' => SNMPTRAPD_DEFAULT_PORT,
118             'Timeout' => 10,
119             'Family' => AF_INET
120             );
121              
122 0 0         if (@_ == 1) {
123 0           $LASTERROR = "Insufficient number of args - @_";
124             return undef
125 0           } else {
126 0           my %cfg = @_;
127 0           for (keys(%cfg)) {
128 0 0         if (/^-?localport$/i) {
    0          
    0          
    0          
129 0           $params{LocalPort} = $cfg{$_}
130             } elsif (/^-?localaddr$/i) {
131 0           $params{LocalAddr} = $cfg{$_}
132             } elsif (/^-?family$/i) {
133 0 0         if ($cfg{$_} =~ /^(?:(?:(:?ip)?v?(?:4|6))|${\AF_INET}|$AF_INET6)$/) {
  0            
134 0 0         if ($cfg{$_} =~ /^(?:(?:(:?ip)?v?4)|${\AF_INET})$/) {
  0            
135 0           $params{Family} = AF_INET
136             } else {
137 0 0         if (!$HAVE_IO_Socket_IP) {
138 0           $LASTERROR = "IO::Socket::IP required for IPv6";
139             return undef
140 0           }
141 0           $params{Family} = $AF_INET6;
142 0 0         if ($^O ne 'MSWin32') {
143 0           $params{V6Only} = 1
144             }
145             }
146             } else {
147 0           $LASTERROR = "Invalid family - $cfg{$_}";
148             return undef
149 0           }
150             } elsif (/^-?timeout$/i) {
151 0 0         if ($cfg{$_} =~ /^\d+$/) {
152 0           $params{Timeout} = $cfg{$_}
153             } else {
154 0           $LASTERROR = "Invalid timeout - $cfg{$_}";
155             return undef
156 0           }
157             # pass through
158             } else {
159 0           $params{$_} = $cfg{$_}
160             }
161             }
162             }
163              
164 0 0         if (my $udpserver = $class->SUPER::new(%params)) {
165 0           return bless {
166             %params, # merge user parameters
167             '_UDPSERVER_' => $udpserver
168             }, $class
169             } else {
170 0           $LASTERROR = "Error opening socket for listener: $@";
171             return undef
172 0           }
173             }
174              
175             sub get_trap {
176 0     0 1   my $self = shift;
177 0   0       my $class = ref($self) || $self;
178              
179 0           my $trap;
180              
181 0           foreach my $key (keys(%{$self})) {
  0            
182             # everything but '_xxx_'
183 0 0         $key =~ /^\_.+\_$/ and next;
184 0           $trap->{$key} = $self->{$key}
185             }
186              
187 0           my $datagramsize = SNMPTRAPD_MAX_SIZE;
188 0 0         if (@_ == 1) {
189 0           $LASTERROR = "Insufficient number of args: @_";
190             return undef
191 0           } else {
192 0           my %args = @_;
193 0           for (keys(%args)) {
194             # -maxsize
195 0 0         if (/^-?(?:max)?size$/i) {
    0          
196 0 0         if ($args{$_} =~ /^\d+$/) {
    0          
    0          
197 0 0 0       if (($args{$_} >= 1) && ($args{$_} <= SNMPTRAPD_MAX_SIZE)) {
198 0           $datagramsize = $args{$_}
199             }
200             } elsif ($args{$_} =~ /^rfc$/i) {
201 0           $datagramsize = SNMPTRAPD_RFC_SIZE
202             } elsif ($args{$_} =~ /^rec(?:ommend)?(?:ed)?$/i) {
203 0           $datagramsize = SNMPTRAPD_REC_SIZE
204             } else {
205 0           $LASTERROR = "Not a valid size: $args{$_}";
206             return undef
207 0           }
208             # -timeout
209             } elsif (/^-?timeout$/i) {
210 0 0         if ($args{$_} =~ /^\d+$/) {
211 0           $trap->{Timeout} = $args{$_}
212             } else {
213 0           $LASTERROR = "Invalid timeout - $args{$_}";
214             return undef
215 0           }
216             }
217             }
218             }
219              
220 0           my $Timeout = $trap->{Timeout};
221 0           my $udpserver = $self->{_UDPSERVER_};
222 0           my $datagram;
223              
224 0 0         if ($Timeout != 0) {
225             # vars for IO select
226 0           my ($rin, $rout, $ein, $eout) = ('', '', '', '');
227 0           vec($rin, fileno($udpserver), 1) = 1;
228              
229             # check if a message is waiting
230 0 0         if (! select($rout=$rin, undef, $eout=$ein, $Timeout)) {
231 0           $LASTERROR = "Timed out waiting for datagram";
232 0           return(0)
233             }
234             }
235              
236             # read the message
237 0 0         if ($udpserver->recv($datagram, $datagramsize)) {
238              
239 0           $trap->{_UDPSERVER_} = $udpserver;
240 0           $trap->{_TRAP_}{PeerPort} = $udpserver->SUPER::peerport;
241 0           $trap->{_TRAP_}{PeerAddr} = $udpserver->SUPER::peerhost;
242 0           $trap->{_TRAP_}{datagram} = $datagram;
243              
244 0           return bless $trap, $class
245             }
246              
247 0           $LASTERROR = sprintf "Socket RECV error: $!";
248             return undef
249 0           }
250              
251             sub process_trap {
252 0     0 1   my $self = shift;
253 0   0       my $class = ref($self) || $self;
254              
255             ### Allow to be called as subroutine
256             # Net::SNMPTrapd->process_trap($data)
257 0 0 0       if (($self eq $class) && ($class eq __PACKAGE__)) {
258 0           my %th;
259 0           $self = \%th;
260 0           ($self->{_TRAP_}{datagram}) = @_
261             }
262             # Net::SNMPTrapd::process_trap($data)
263 0 0         if ($class ne __PACKAGE__) {
264 0           my %th;
265 0           $self = \%th;
266 0           ($self->{_TRAP_}{datagram}) = $class;
267 0           $class = __PACKAGE__
268             }
269              
270 0           my $RESPONSE = 1; # Default is to send Response PDU for InformRequest
271             # If more than 1 argument, parse the options
272 0 0         if (@_ != 1) {
273 0           my %args = @_;
274 0           for (keys(%args)) {
275             # -datagram
276 0 0 0       if ((/^-?data(?:gram)?$/i) || (/^-?pdu$/i)) {
    0          
277 0           $self->{_TRAP_}{datagram} = $args{$_}
278             # -noresponse
279             } elsif (/^-?noresponse$/i) {
280 0 0 0       if (($args{$_} =~ /^\d+$/) && ($args{$_} > 0)) {
281 0           $RESPONSE = 0
282             }
283             }
284             }
285             }
286              
287 0           my $trap;
288 0 0         if (!defined($trap = $snmpasn->decode($self->{_TRAP_}{datagram}))) {
289 0 0         $LASTERROR = sprintf "Error decoding PDU - %s", (defined($snmpasn->error) ? $snmpasn->error : "Unknown Convert::ASN1->decode() error. Consider $class dump()");
290             return undef
291 0           }
292             #DEBUG: use Data::Dumper; print Dumper \$trap;
293              
294             # Only understand SNMPv1 (0) and v2c (1)
295 0 0         if ($trap->{version} > 1) {
296 0           $LASTERROR = sprintf "Unrecognized SNMP version - %i", $trap->{version};
297             return undef
298 0           }
299              
300             # set PDU Type for later use
301 0           my $pdutype = sprintf "%s", keys(%{$trap->{pdu_type}});
  0            
302              
303             ### Assemble decoded trap object
304             # Common
305 0           $self->{_TRAP_}{version} = $trap->{version};
306 0           $self->{_TRAP_}{community} = $trap->{community};
307 0 0         if ($pdutype eq 'trap') {
    0          
    0          
308 0           $self->{_TRAP_}{pdu_type} = 4
309            
310             } elsif ($pdutype eq 'inform_request') {
311 0           $self->{_TRAP_}{pdu_type} = 6;
312              
313             # send response for InformRequest
314 0 0         if ($RESPONSE) {
315 0 0         if ((my $r = _InformRequest_Response(\$self, $trap, $pdutype)) ne 'OK') {
316 0           $LASTERROR = sprintf "Error sending InformRequest Response - %s", $r;
317             return undef
318 0           }
319             }
320              
321             } elsif ($pdutype eq 'snmpv2_trap') {
322 0           $self->{_TRAP_}{pdu_type} = 7
323             }
324              
325             # v1
326 0 0         if ($trap->{version} == 0) {
    0          
327 0           $self->{_TRAP_}{ent_oid} = $trap->{pdu_type}->{$pdutype}->{ent_oid};
328 0           $self->{_TRAP_}{agent_addr} = _inetNtoa($trap->{pdu_type}->{$pdutype}->{agent_addr});
329 0           $self->{_TRAP_}{generic_trap} = $trap->{pdu_type}->{$pdutype}->{generic_trap};
330 0           $self->{_TRAP_}{specific_trap} = $trap->{pdu_type}->{$pdutype}->{specific_trap};
331 0           $self->{_TRAP_}{timeticks} = $trap->{pdu_type}->{$pdutype}->{timeticks};
332              
333             # v2c
334             } elsif ($trap->{version} == 1) {
335 0           $self->{_TRAP_}{request_id} = $trap->{pdu_type}->{$pdutype}->{request_id};
336 0           $self->{_TRAP_}{error_status} = $trap->{pdu_type}->{$pdutype}->{error_status};
337 0           $self->{_TRAP_}{error_index} = $trap->{pdu_type}->{$pdutype}->{error_index};
338             }
339              
340             # varbinds
341 0           my @varbinds;
342 0           for my $i (0..$#{$trap->{pdu_type}->{$pdutype}->{varbindlist}}) {
  0            
343 0           my %oidval;
344 0           for (keys(%{$trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{value}})) {
  0            
345             # defined
346 0 0         if (defined($trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{value}{$_})) {
347             # special cases: IP address, null
348 0 0         if ($_ eq 'ipaddr') {
    0          
349 0           $oidval{$trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{oid}} = _inetNtoa($trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{value}{$_})
350             } elsif ($_ eq 'null') {
351 0           $oidval{$trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{oid}} = '(NULL)'
352             # no special case: just assign it
353             } else {
354 0           $oidval{$trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{oid}} = $trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{value}{$_}
355             }
356             # not defined - ""
357             } else {
358 0           $oidval{$trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{oid}} = ""
359             }
360             }
361 0           push @varbinds, \%oidval
362             }
363 0           $self->{_TRAP_}{varbinds} = \@varbinds;
364              
365 0           return bless $self, $class
366             }
367              
368             sub server {
369 0     0 1   my $self = shift;
370             return $self->{_UDPSERVER_}
371 0           }
372              
373             sub datagram {
374 0     0 1   my ($self, $arg) = @_;
375              
376 0 0 0       if (defined($arg) && ($arg >= 1)) {
377             return unpack ('H*', $self->{_TRAP_}{datagram})
378 0           } else {
379             return $self->{_TRAP_}{datagram}
380 0           }
381             }
382              
383             sub remoteaddr {
384 0     0 1   my $self = shift;
385             return $self->{_TRAP_}{PeerAddr}
386 0           }
387              
388             sub remoteport {
389 0     0 1   my $self = shift;
390             return $self->{_TRAP_}{PeerPort}
391 0           }
392              
393             sub version {
394 0     0 1   my $self = shift;
395 0           return $self->{_TRAP_}{version} + 1
396             }
397              
398             sub community {
399 0     0 1   my $self = shift;
400             return $self->{_TRAP_}{community}
401 0           }
402              
403             sub pdu_type {
404 0     0 1   my ($self, $arg) = @_;
405              
406 0 0 0       if (defined($arg) && ($arg >= 1)) {
407             return $self->{_TRAP_}{pdu_type}
408 0           } else {
409 0           return $PDUTYPES[$self->{_TRAP_}{pdu_type}]
410             }
411             }
412              
413             sub ent_OID {
414 0     0 1   my $self = shift;
415             return $self->{_TRAP_}{ent_oid}
416 0           }
417              
418             sub agentaddr {
419 0     0 1   my $self = shift;
420             return $self->{_TRAP_}{agent_addr}
421 0           }
422              
423             sub generic_trap {
424 0     0 1   my ($self, $arg) = @_;
425              
426 0 0 0       if (defined($arg) && ($arg >= 1)) {
427             return $self->{_TRAP_}{generic_trap}
428 0           } else {
429 0           return $TRAPTYPES[$self->{_TRAP_}{generic_trap}]
430             }
431             }
432              
433             sub specific_trap {
434 0     0 1   my $self = shift;
435             return $self->{_TRAP_}{specific_trap}
436 0           }
437              
438             sub timeticks {
439 0     0 1   my $self = shift;
440             return $self->{_TRAP_}{timeticks}
441 0           }
442              
443             sub request_ID {
444 0     0 1   my $self = shift;
445             return $self->{_TRAP_}{request_id}
446 0           }
447              
448             sub error_status {
449 0     0 1   my $self = shift;
450             return $self->{_TRAP_}{error_status}
451 0           }
452              
453             sub error_index {
454 0     0 1   my $self = shift;
455             return $self->{_TRAP_}{error_index}
456 0           }
457              
458             sub varbinds {
459 0     0 1   my $self = shift;
460             return $self->{_TRAP_}{varbinds}
461 0           }
462              
463             sub error {
464 0     0 1   return $LASTERROR
465             }
466              
467             sub dump {
468 0     0 1   my $self = shift;
469 0   0       my $class = ref($self) || $self;
470              
471             ### Allow to be called as subroutine
472             # Net::SNMPTrapd->dump($datagram)
473 0 0 0       if (($self eq $class) && ($class eq __PACKAGE__)) {
474 0           my %th;
475 0           $self = \%th;
476 0           ($self->{_TRAP_}{datagram}) = @_
477             }
478             # Net::SNMPTrapd::dump($datagram)
479 0 0         if ($class ne __PACKAGE__) {
480 0           my %th;
481 0           $self = \%th;
482 0           ($self->{_TRAP_}{datagram}) = $class;
483 0           $class = __PACKAGE__
484             }
485              
486 0 0         if (defined($self->{_TRAP_}{datagram})) {
487 0           Convert::ASN1::asn_dump($self->{_TRAP_}{datagram});
488 0           Convert::ASN1::asn_hexdump($self->{_TRAP_}{datagram});
489             } else {
490 0           $LASTERROR = "Missing datagram to dump";
491             return undef
492 0           }
493              
494 0           return 1
495             }
496              
497             ########################################################
498             # End Public Module
499             ########################################################
500              
501             ########################################################
502             # Start Private subs
503             ########################################################
504              
505             sub _InformRequest_Response {
506              
507 0     0     my ($self, $trap, $pdutype) = @_;
508 0   0       my $class = ref($$self) || $$self;
509              
510             #DEBUG print "BUFFER = $buffer\n";
511 0 0         if (!defined $$self->{_UDPSERVER_}) {
512 0           return "Server not defined"
513             }
514              
515             # Change from request to response
516 0           $trap->{pdu_type}{response} = delete $trap->{pdu_type}{inform_request};
517 0           my $buffer = $snmpasn->encode($trap);
518 0 0         if (!defined($buffer)) {
519 0           return $snmpasn->error
520             }
521              
522             # send Inform response
523 0           my $socket = $$self->{_UDPSERVER_};
524 0           $socket->send($buffer);
525              
526             # Change back to request from response
527 0           $trap->{pdu_type}{inform_request} = delete $trap->{pdu_type}{response};
528 0           return "OK"
529             }
530              
531             sub _inetNtoa {
532 0     0     my ($addr) = @_;
533              
534 0 0         if (Version->parse($Socket::VERSION) >= Version->parse(1.94)) {
535 0           my $name;
536 0 0         if (length($addr) == 4) {
537 0           $name = Socket::pack_sockaddr_in(0, $addr)
538             } else {
539 0           $name = Socket::pack_sockaddr_in6(0, $addr)
540             }
541 0           my ($err, $address) = Socket::getnameinfo($name, $NI_NUMERICHOST);
542 0 0         if (defined($address)) {
543 0           return $address
544             } else {
545 0           $LASTERROR = "getnameinfo($addr) failed - $err";
546             return undef
547 0           }
548             } else {
549 0 0         if (length($addr) == 4) {
550 0           return inet_ntoa($addr)
551             } else {
552             # Poor man's IPv6
553 0           return join ':', (unpack '(a4)*', unpack ('H*', $addr))
554             }
555             }
556             }
557              
558             ########################################################
559             # End Private subs
560             ########################################################
561              
562             1;
563              
564             __END__