package Net::SNMPTrapd;
########################################################
# AUTHOR = Michael Vincent
# www.VinsWorld.com
########################################################
use strict;
use warnings;
use Convert::ASN1;
use Socket qw(inet_ntoa AF_INET IPPROTO_TCP);
my $AF_INET6 = eval { Socket::AF_INET6() };
my $NI_NUMERICHOST = eval { Socket::NI_NUMERICHOST() };
our $VERSION = '0.16';
our @ISA;
my $HAVE_IO_Socket_IP = 0;
eval "use IO::Socket::IP -register";
if(!$@) {
$HAVE_IO_Socket_IP = 1;
push @ISA, "IO::Socket::IP"
} else {
require IO::Socket::INET;
push @ISA, "IO::Socket::INET";
}
########################################################
# Start Variables
########################################################
use constant SNMPTRAPD_DEFAULT_PORT => 162;
use constant SNMPTRAPD_RFC_SIZE => 484; # RFC limit
use constant SNMPTRAPD_REC_SIZE => 1472; # Recommended size
use constant SNMPTRAPD_MAX_SIZE => 65467; # Actual limit (65535 - IP/UDP)
my @TRAPTYPES = qw(COLDSTART WARMSTART LINKDOWN LINKUP AUTHFAIL EGPNEIGHBORLOSS ENTERPRISESPECIFIC);
my @PDUTYPES = qw(GetRequest GetNextRequest Response SetRequest Trap GetBulkRequest InformRequest SNMPv2-Trap Report);
our $LASTERROR;
my $asn = Convert::ASN1->new;
$asn->prepare("
PDU ::= SEQUENCE {
version INTEGER,
community STRING,
pdu_type PDUs
}
PDUs ::= CHOICE {
response Response_PDU,
trap Trap_PDU,
inform_request InformRequest_PDU,
snmpv2_trap SNMPv2_Trap_PDU
}
Response_PDU ::= [2] IMPLICIT PDUv2
Trap_PDU ::= [4] IMPLICIT PDUv1
InformRequest_PDU ::= [6] IMPLICIT PDUv2
SNMPv2_Trap_PDU ::= [7] IMPLICIT PDUv2
IPAddress ::= [APPLICATION 0] STRING
Counter32 ::= [APPLICATION 1] INTEGER
Guage32 ::= [APPLICATION 2] INTEGER
TimeTicks ::= [APPLICATION 3] INTEGER
Opaque ::= [APPLICATION 4] STRING
Counter64 ::= [APPLICATION 6] INTEGER
PDUv1 ::= SEQUENCE {
ent_oid OBJECT IDENTIFIER,
agent_addr IPAddress,
generic_trap INTEGER,
specific_trap INTEGER,
timeticks TimeTicks,
varbindlist VARBINDS
}
PDUv2 ::= SEQUENCE {
request_id INTEGER,
error_status INTEGER,
error_index INTEGER,
varbindlist VARBINDS
}
VARBINDS ::= SEQUENCE OF SEQUENCE {
oid OBJECT IDENTIFIER,
value CHOICE {
integer INTEGER,
string STRING,
oid OBJECT IDENTIFIER,
ipaddr IPAddress,
counter32 Counter32,
guage32 Guage32,
timeticks TimeTicks,
opaque Opaque,
counter64 Counter64,
null NULL
}
}
");
my $snmpasn = $asn->find('PDU');
########################################################
# End Variables
########################################################
########################################################
# Start Public Module
########################################################
sub new {
my $self = shift;
my $class = ref($self) || $self;
# Default parameters
my %params = (
'Proto' => 'udp',
'LocalPort' => SNMPTRAPD_DEFAULT_PORT,
'Timeout' => 10,
'Family' => AF_INET
);
if (@_ == 1) {
$LASTERROR = "Insufficient number of args - @_";
return undef
} else {
my %cfg = @_;
for (keys(%cfg)) {
if (/^-?localport$/i) {
$params{LocalPort} = $cfg{$_}
} elsif (/^-?localaddr$/i) {
$params{LocalAddr} = $cfg{$_}
} elsif (/^-?family$/i) {
if ($cfg{$_} =~ /^(?:(?:(:?ip)?v?(?:4|6))|${\AF_INET}|$AF_INET6)$/) {
if ($cfg{$_} =~ /^(?:(?:(:?ip)?v?4)|${\AF_INET})$/) {
$params{Family} = AF_INET
} else {
if (!$HAVE_IO_Socket_IP) {
$LASTERROR = "IO::Socket::IP required for IPv6";
return undef
}
$params{Family} = $AF_INET6;
if ($^O ne 'MSWin32') {
$params{V6Only} = 1
}
}
} else {
$LASTERROR = "Invalid family - $cfg{$_}";
return undef
}
} elsif (/^-?timeout$/i) {
if ($cfg{$_} =~ /^\d+$/) {
$params{Timeout} = $cfg{$_}
} else {
$LASTERROR = "Invalid timeout - $cfg{$_}";
return undef
}
# pass through
} else {
$params{$_} = $cfg{$_}
}
}
}
if (my $udpserver = $class->SUPER::new(%params)) {
return bless {
%params, # merge user parameters
'_UDPSERVER_' => $udpserver
}, $class
} else {
$LASTERROR = "Error opening socket for listener: $@";
return undef
}
}
sub get_trap {
my $self = shift;
my $class = ref($self) || $self;
my $trap;
foreach my $key (keys(%{$self})) {
# everything but '_xxx_'
$key =~ /^\_.+\_$/ and next;
$trap->{$key} = $self->{$key}
}
my $datagramsize = SNMPTRAPD_MAX_SIZE;
if (@_ == 1) {
$LASTERROR = "Insufficient number of args: @_";
return undef
} else {
my %args = @_;
for (keys(%args)) {
# -maxsize
if (/^-?(?:max)?size$/i) {
if ($args{$_} =~ /^\d+$/) {
if (($args{$_} >= 1) && ($args{$_} <= SNMPTRAPD_MAX_SIZE)) {
$datagramsize = $args{$_}
}
} elsif ($args{$_} =~ /^rfc$/i) {
$datagramsize = SNMPTRAPD_RFC_SIZE
} elsif ($args{$_} =~ /^rec(?:ommend)?(?:ed)?$/i) {
$datagramsize = SNMPTRAPD_REC_SIZE
} else {
$LASTERROR = "Not a valid size: $args{$_}";
return undef
}
# -timeout
} elsif (/^-?timeout$/i) {
if ($args{$_} =~ /^\d+$/) {
$trap->{Timeout} = $args{$_}
} else {
$LASTERROR = "Invalid timeout - $args{$_}";
return undef
}
}
}
}
my $Timeout = $trap->{Timeout};
my $udpserver = $self->{_UDPSERVER_};
my $datagram;
if ($Timeout != 0) {
# vars for IO select
my ($rin, $rout, $ein, $eout) = ('', '', '', '');
vec($rin, fileno($udpserver), 1) = 1;
# check if a message is waiting
if (! select($rout=$rin, undef, $eout=$ein, $Timeout)) {
$LASTERROR = "Timed out waiting for datagram";
return(0)
}
}
# read the message
if ($udpserver->recv($datagram, $datagramsize)) {
$trap->{_UDPSERVER_} = $udpserver;
$trap->{_TRAP_}{PeerPort} = $udpserver->SUPER::peerport;
$trap->{_TRAP_}{PeerAddr} = $udpserver->SUPER::peerhost;
$trap->{_TRAP_}{datagram} = $datagram;
return bless $trap, $class
}
$LASTERROR = sprintf "Socket RECV error: $!";
return undef
}
sub process_trap {
my $self = shift;
my $class = ref($self) || $self;
### Allow to be called as subroutine
# Net::SNMPTrapd->process_trap($data)
if (($self eq $class) && ($class eq __PACKAGE__)) {
my %th;
$self = \%th;
($self->{_TRAP_}{datagram}) = @_
}
# Net::SNMPTrapd::process_trap($data)
if ($class ne __PACKAGE__) {
my %th;
$self = \%th;
($self->{_TRAP_}{datagram}) = $class;
$class = __PACKAGE__
}
my $RESPONSE = 1; # Default is to send Response PDU for InformRequest
# If more than 1 argument, parse the options
if (@_ != 1) {
my %args = @_;
for (keys(%args)) {
# -datagram
if ((/^-?data(?:gram)?$/i) || (/^-?pdu$/i)) {
$self->{_TRAP_}{datagram} = $args{$_}
# -noresponse
} elsif (/^-?noresponse$/i) {
if (($args{$_} =~ /^\d+$/) && ($args{$_} > 0)) {
$RESPONSE = 0
}
}
}
}
my $trap;
if (!defined($trap = $snmpasn->decode($self->{_TRAP_}{datagram}))) {
$LASTERROR = sprintf "Error decoding PDU - %s", (defined($snmpasn->error) ? $snmpasn->error : "Unknown Convert::ASN1->decode() error. Consider $class dump()");
return undef
}
#DEBUG: use Data::Dumper; print Dumper \$trap;
# Only understand SNMPv1 (0) and v2c (1)
if ($trap->{version} > 1) {
$LASTERROR = sprintf "Unrecognized SNMP version - %i", $trap->{version};
return undef
}
# set PDU Type for later use
my $pdutype = sprintf "%s", keys(%{$trap->{pdu_type}});
### Assemble decoded trap object
# Common
$self->{_TRAP_}{version} = $trap->{version};
$self->{_TRAP_}{community} = $trap->{community};
if ($pdutype eq 'trap') {
$self->{_TRAP_}{pdu_type} = 4
} elsif ($pdutype eq 'inform_request') {
$self->{_TRAP_}{pdu_type} = 6;
# send response for InformRequest
if ($RESPONSE) {
if ((my $r = _InformRequest_Response(\$self, $trap, $pdutype)) ne 'OK') {
$LASTERROR = sprintf "Error sending InformRequest Response - %s", $r;
return undef
}
}
} elsif ($pdutype eq 'snmpv2_trap') {
$self->{_TRAP_}{pdu_type} = 7
}
# v1
if ($trap->{version} == 0) {
$self->{_TRAP_}{ent_oid} = $trap->{pdu_type}->{$pdutype}->{ent_oid};
$self->{_TRAP_}{agent_addr} = _inetNtoa($trap->{pdu_type}->{$pdutype}->{agent_addr});
$self->{_TRAP_}{generic_trap} = $trap->{pdu_type}->{$pdutype}->{generic_trap};
$self->{_TRAP_}{specific_trap} = $trap->{pdu_type}->{$pdutype}->{specific_trap};
$self->{_TRAP_}{timeticks} = $trap->{pdu_type}->{$pdutype}->{timeticks};
# v2c
} elsif ($trap->{version} == 1) {
$self->{_TRAP_}{request_id} = $trap->{pdu_type}->{$pdutype}->{request_id};
$self->{_TRAP_}{error_status} = $trap->{pdu_type}->{$pdutype}->{error_status};
$self->{_TRAP_}{error_index} = $trap->{pdu_type}->{$pdutype}->{error_index};
}
# varbinds
my @varbinds;
for my $i (0..$#{$trap->{pdu_type}->{$pdutype}->{varbindlist}}) {
my %oidval;
for (keys(%{$trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{value}})) {
# defined
if (defined($trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{value}{$_})) {
# special cases: IP address, null
if ($_ eq 'ipaddr') {
$oidval{$trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{oid}} = _inetNtoa($trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{value}{$_})
} elsif ($_ eq 'null') {
$oidval{$trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{oid}} = '(NULL)'
# no special case: just assign it
} else {
$oidval{$trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{oid}} = $trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{value}{$_}
}
# not defined - ""
} else {
$oidval{$trap->{pdu_type}->{$pdutype}->{varbindlist}[$i]->{oid}} = ""
}
}
push @varbinds, \%oidval
}
$self->{_TRAP_}{varbinds} = \@varbinds;
return bless $self, $class
}
sub server {
my $self = shift;
return $self->{_UDPSERVER_}
}
sub datagram {
my ($self, $arg) = @_;
if (defined($arg) && ($arg >= 1)) {
return unpack ('H*', $self->{_TRAP_}{datagram})
} else {
return $self->{_TRAP_}{datagram}
}
}
sub remoteaddr {
my $self = shift;
return $self->{_TRAP_}{PeerAddr}
}
sub remoteport {
my $self = shift;
return $self->{_TRAP_}{PeerPort}
}
sub version {
my $self = shift;
return $self->{_TRAP_}{version} + 1
}
sub community {
my $self = shift;
return $self->{_TRAP_}{community}
}
sub pdu_type {
my ($self, $arg) = @_;
if (defined($arg) && ($arg >= 1)) {
return $self->{_TRAP_}{pdu_type}
} else {
return $PDUTYPES[$self->{_TRAP_}{pdu_type}]
}
}
sub ent_OID {
my $self = shift;
return $self->{_TRAP_}{ent_oid}
}
sub agentaddr {
my $self = shift;
return $self->{_TRAP_}{agent_addr}
}
sub generic_trap {
my ($self, $arg) = @_;
if (defined($arg) && ($arg >= 1)) {
return $self->{_TRAP_}{generic_trap}
} else {
return $TRAPTYPES[$self->{_TRAP_}{generic_trap}]
}
}
sub specific_trap {
my $self = shift;
return $self->{_TRAP_}{specific_trap}
}
sub timeticks {
my $self = shift;
return $self->{_TRAP_}{timeticks}
}
sub request_ID {
my $self = shift;
return $self->{_TRAP_}{request_id}
}
sub error_status {
my $self = shift;
return $self->{_TRAP_}{error_status}
}
sub error_index {
my $self = shift;
return $self->{_TRAP_}{error_index}
}
sub varbinds {
my $self = shift;
return $self->{_TRAP_}{varbinds}
}
sub error {
return $LASTERROR
}
sub dump {
my $self = shift;
my $class = ref($self) || $self;
### Allow to be called as subroutine
# Net::SNMPTrapd->dump($datagram)
if (($self eq $class) && ($class eq __PACKAGE__)) {
my %th;
$self = \%th;
($self->{_TRAP_}{datagram}) = @_
}
# Net::SNMPTrapd::dump($datagram)
if ($class ne __PACKAGE__) {
my %th;
$self = \%th;
($self->{_TRAP_}{datagram}) = $class;
$class = __PACKAGE__
}
if (defined($self->{_TRAP_}{datagram})) {
Convert::ASN1::asn_dump($self->{_TRAP_}{datagram});
Convert::ASN1::asn_hexdump($self->{_TRAP_}{datagram});
} else {
$LASTERROR = "Missing datagram to dump";
return undef
}
return 1
}
########################################################
# End Public Module
########################################################
########################################################
# Start Private subs
########################################################
sub _InformRequest_Response {
my ($self, $trap, $pdutype) = @_;
my $class = ref($$self) || $$self;
#DEBUG print "BUFFER = $buffer\n";
if (!defined $$self->{_UDPSERVER_}) {
return "Server not defined"
}
# Change from request to response
$trap->{pdu_type}{response} = delete $trap->{pdu_type}{inform_request};
my $buffer = $snmpasn->encode($trap);
if (!defined($buffer)) {
return $snmpasn->error
}
# send Inform response
my $socket = $$self->{_UDPSERVER_};
$socket->send($buffer);
# Change back to request from response
$trap->{pdu_type}{inform_request} = delete $trap->{pdu_type}{response};
return "OK"
}
sub _inetNtoa {
my ($addr) = @_;
if ($Socket::VERSION >= 1.94) {
my $name;
if (length($addr) == 4) {
$name = Socket::pack_sockaddr_in(0, $addr)
} else {
$name = Socket::pack_sockaddr_in6(0, $addr)
}
my ($err, $address) = Socket::getnameinfo($name, $NI_NUMERICHOST);
if (defined($address)) {
return $address
} else {
$LASTERROR = "getnameinfo($addr) failed - $err";
return undef
}
} else {
if (length($addr) == 4) {
return inet_ntoa($addr)
} else {
# Poor man's IPv6
return join ':', (unpack '(a4)*', unpack ('H*', $addr))
}
}
}
########################################################
# End Private subs
########################################################
1;
__END__
########################################################
# Start POD
########################################################
=head1 NAME
Net::SNMPTrapd - Perl implementation of SNMP Trap Listener
=head1 SYNOPSIS
use Net::SNMPTrapd;
my $snmptrapd = Net::SNMPTrapd->new()
or die "Error creating SNMPTrapd listener: ", Net::SNMPTrapd->error;
while (1) {
my $trap = $snmptrapd->get_trap();
if (!defined($trap)) {
printf "$0: %s\n", Net::SNMPTrapd->error;
exit 1
} elsif ($trap == 0) {
next
}
if (!defined($trap->process_trap())) {
printf "$0: %s\n", Net::SNMPTrapd->error
} else {
printf "%s\t%i\t%i\t%s\n",
$trap->remoteaddr,
$trap->remoteport,
$trap->version,
$trap->community
}
}
=head1 DESCRIPTION
Net::SNMPTrapd is a class implementing a simple SNMP Trap listener in
Perl. Net::SNMPTrapd will accept traps on the default SNMP Trap port
(UDP 162) and attempt to decode them. Net::SNMPTrapd supports SNMP v1
and v2c traps and SNMPv2 InformRequest and implements the Reponse.
Net::SNMPTrapd uses Convert::ASN1 by Graham Barr to do the decoding.
=head1 METHODS
=head2 new() - create a new Net::SNMPTrapd object
my $snmptrapd = Net::SNMPTrapd->new([OPTIONS]);
Create a new Net::SNMPTrapd object with OPTIONS as optional parameters.
Valid options are:
Option Description Default
------ ----------- -------
-Family Address family IPv4/IPv6 IPv4
Valid values for IPv4:
4, v4, ip4, ipv4, AF_INET (constant)
Valid values for IPv6:
6, v6, ip6, ipv6, AF_INET6 (constant)
-LocalAddr Interface to bind to any
-LocalPort Port to bind server to 162
-timeout Timeout in seconds for socket 10
operations and to wait for request
B<NOTE>: IPv6 requires B<IO::Socket::IP>. Failback is B<IO::Socket::INET>
and only IPv4 support.
Allows the following accessors to be called.
=head3 server() - return IO::Socket::IP object for server
$snmptrapd->server();
Return B<IO::Socket::IP> object for the created server.
All B<IO::Socket::IP> accessors can then be called.
=head2 get_trap() - listen for SNMP traps
my $trap = $snmptrapd->get_trap([OPTIONS]);
Listen for SNMP traps. Timeout after default or user specified
timeout set in C<new> method and return '0'. If trap is received
before timeout, return is defined. Return is not defined if error
encountered.
Valid options are:
Option Description Default
------ ----------- -------
-maxsize Max size in bytes of acceptable PDU. 65467
Value can be integer 1 <= # <= 65467.
Keywords: 'RFC' = 484
'recommended' = 1472
-timeout Timeout in seconds to wait for 10
request. Overrides value set with
new().
Allows the following accessors to be called.
=head3 remoteaddr() - return remote address from SNMP trap
$trap->remoteaddr();
Return remote address value from a received (C<get_trap()>)
SNMP trap. This is the address from the IP header on the UDP
datagram.
=head3 remoteport() - return remote port from SNMP trap
$trap->remoteport();
Return remote port value from a received (C<get_trap()>)
SNMP trap. This is the port from the IP header on the UDP
datagram.
=head3 datagram() - return datagram from SNMP trap
$trap->datagram([1]);
Return the raw datagram from a received (C<get_trap()>)
SNMP trap. This is ASN.1 encoded datagram. For a hex
dump, use the optional boolean argument.
=head2 process_trap() - process received SNMP trap
$trap->process_trap([OPTIONS]);
Process a received SNMP trap. Decodes the received (C<get_trap()>)
PDU. Varbinds are extracted and decoded. If PDU is SNMPv2
InformRequest, the Response PDU is generated and sent to IP
address and UDP port found in the original datagram header
(C<get_trap()> methods C<remoteaddr()> and C<remoteport()>).
Called with one argument, interpreted as the datagram to process.
Valid options are:
Option Description Default
------ ----------- -------
-datagram Datagram to process -Provided by
get_trap()-
-noresponse Binary switch (0|1) meaning 'Do not 0
send Response-PDU for InformRequest' -Send Response-
This can also be called as a procedure if one is inclined to write
their own UDP listener instead of using C<get_trap()>. For example:
$sock = IO::Socket::IP->new( blah blah blah );
$sock->recv($datagram, 1500);
# process the ASN.1 encoded datagram in $datagram variable
$trap = Net::SNMPTrapd->process_trap($datagram);
or
# process the ASN.1 encoded datagram in $datagram variable
# Do *NOT* send Response PDU if trap comes as InformRequest PDU
$trap = Net::SNMPTrapd->process_trap(
-datagram => $datagram,
-noresponse => 1
);
In any instantiation, allows the following accessors to be called.
=head3 version() - return version from SNMP trap
$trap->version();
Return SNMP Trap version from a received and processed
(C<process_trap()>) SNMP trap.
B<NOTE:> This module only supports SNMP v1 and v2c.
=head3 community() - return community from SNMP trap
$trap->community();
Return community string from a received and processed
(C<process_trap()>) SNMP trap.
=head3 pdu_type() - return PDU type from SNMP trap
$trap->pdu_type([1]);
Return PDU type from a received and processed (C<process_trap()>)
SNMP trap. This is the text representation of the PDU type.
For the raw number, use the optional boolean argument.
=head3 varbinds() - return varbinds from SNMP trap
$trap->varbinds();
Return varbinds from a received and processed
(C<process_trap()>) SNMP trap. This returns a pointer to an array
containing a hash as each array element. The key/value pairs of
each hash are the OID/value pairs for each varbind in the received
trap.
[{OID => value}]
[{OID => value}]
...
[{OID => value}]
An example extraction of the varbind data is provided:
for my $vals (@{$trap->varbinds}) {
for (keys(%{$vals})) {
$p .= sprintf "%s: %s; ", $_, $vals->{$_}
}
}
print "$p\n";
The above code will print the varbinds as:
OID: value; OID: value; OID: value; [...]
=head3 SNMP v1 SPECIFIC
The following methods are SNMP v1 trap specific.
=head3 ent_OID() - return enterprise OID from SNMP v1 trap
$trap->ent_OID();
Return enterprise OID from a received and processed
(C<process_trap()>) SNMP v1 trap.
=head3 agentaddr() - return agent address from SNMP v1 trap
$trap->agentaddr();
Return agent address from a received and processed
(C<process_trap()>) SNMP v1 trap.
=head3 generic_trap() - return generic trap from SNMP v1 trap
$trap->generic_trap([1]);
Return generic trap type from a received and processed
(C<process_trap()>) SNMP v1 trap. This is the text representation
of the generic trap type. For the raw number, use the optional
boolean argument.
=head3 specific_trap() - return specific trap from SNMP v1 trap
$trap->specific_trap();
Return specific trap type from a received and processed
(C<process_trap()>) SNMP v1 trap.
=head3 timeticks() - return timeticks from SNMP v1 trap
$trap->timeticks();
Return timeticks from a received and processed
(C<process_trap()>) SNMP v1 trap.
=head3 SNMP v2c SPECIFIC
The following methods are SNMP v2c trap specific.
=head3 request_ID() - return request ID from SNMP v2c trap
$trap->request_ID();
Return request ID from a received and processed
(C<process_trap()>) SNMP v2c trap.
=head3 error_status() - return error status from SNMP v2c trap
$trap->error_status();
Return error_status from a received and processed
(C<process_trap()>) SNMP v2c trap.
=head3 error_index() - return error index from SNMP v2c trap
$trap->error_index();
Return error index from a received and processed
(C<process_trap()>) SNMP v2c trap.
=head2 error() - return last error
printf "Error: %s\n", Net::SNMPTrapd->error;
Return last error.
=head2 dump() - Convert::ASN1 direct decode and hex dump
$trap->dump();
or
Net::SNMPTrapd->dump($datagram);
This does B<not> use any of the Net::SNMPTrapd ASN.1 structures;
rather, it uses the Convert::ASN1 module debug routines (C<asn_dump>
and C<asn_hexdump>) to attempt a decode and hex dump of the supplied
datagram. This is helpful to eliminate the entire Net::SNMPTrapd
module code when troubleshooting issues with decoding and focus solely
on the ASN.1 decode of the given datagram.
Called as a method, operates on the value returned from the
C<datagram()> method. Called as a subroutine, operates on the
value passed.
Output is printed directly to STDERR. Return is defined unless there
is an error encountered in getting the datagram to operate on.
=head1 EXPORT
None by default.
=head1 EXAMPLES
This distribution comes with several scripts (installed to the default
"bin" install directory) that not only demonstrate example uses but also
provide functional execution.
=head1 SEE ALSO
Convert::ASN1
=head1 LICENSE
This software is released under the same terms as Perl itself.
If you don't know what that means visit L<http://perl.com/>.
=head1 AUTHOR
Copyright (C) Michael Vincent 2010
L<http://www.VinsWorld.com>
All rights reserved
=cut