7
votes

A few weeks ago I wrote an SNMP relayer for our ops group. They have some dumb devices that can only send traps to a single IP, and we have a monitoring system that listens on multiple IPs for availability. The code's dead simple, and essentially:

while (recv($packet)) {
  foreach $target (@targets) {
    send($target, $packet);
  }
}

It's worked, basically, but now the obvious short coming that it doesn't include the originator IP is an issue (apparently the first class of device included the info as a varbind and some new class does not).

What I would like to do is change my code to something like this:

while ($server->recv($packet)) {
  my $obj = decompile($packet)
  if (!$obj->{varbind}{snmpTrapAddress}) {
    $obj->{varbind}{snmpTrapAddress} = inet_ntoa($server->peeraddr());
  }
  $packet = compile($obj);
  foreach $target (@targets) {
    send($target, $packet);
  }
}

In other words, if my sender isn't including snmpTrapAddress, add it. The problem is that every SNMP package I've looked at for Perl seems very heavily focused on the infrastructure of receiving traps and performing gets.

So: Is there a simple Perl module that will allow me to say "here's a blob of data representing an snmp trap. decode it into something I can easily manipulate, then recompile it back into a blob I can throw over the network"?

If the answer you give is "use SNMP dummy", can you provide examples of this? I may just be blind, but from the output of perldoc SNMP it's not obvious to me how to use it in this manner.

EDIT:

Turns out after looking around a bit that "SNMP encoding" is really ASN.1 BER (Basic Encoding Rules). Based on this I'm having a go with Convert::BER. I would still welcome any easy break down/edit/rebuild tips for SNMP.

3
I don't know anything about SNMP but Net-SNMP has a Net::SNMP::Message class.Sinan Ünür

3 Answers

8
votes

I never found a perfect solution to this. Net::SNMP::Message (part of Net::SNMP) might allow this but doesn't seem to have a publicly defined interface, and none of the Net::SNMP interface seemed especially relevant. NSNMP is closest to the spirit of what I was looking for, but it's brittle and didn't work for my packet out of the box and if I'm going to support brittle code, it's going to be my own brittle code =).

Mon::SNMP also got close to what I was looking for, but it too was broken out of the box. It appears to be abandoned, with the last release in 2001 and the developer's last CPAN release in 2002. I didn't realize it at the time but I now think that it's broken because of a change in the interface to the Convert::BER module it uses.

Mon::SNMP got me pointed toward Convert::BER. A few thousand reads of the Convert::BER POD, the Mon::SNMP source, and RFC 1157 (esp. 4.1.6, "The Trap-PDU") later and I came up with this code as a proof of concept to do what I wanted. This is just proof of concept (for reasons I'll detail after the code) so it may not be perfect, but I thought it might provide useful reference for future Perl people working in this area, so here it is:

#!/usr/bin/perl

use Convert::BER;
use Convert::BER qw(/^(\$|BER_)/);

my $ber = Convert::BER->new();

# OID I want to add to the trap if not already present
my $snmpTrapAddress = '1.3.6.1.6.3.18.1.3';

# this would be from the incoming socket in production
my $source_ip = '10.137.54.253';

# convert the octets into chars to match SNMP standard for IPs
my $source_ip_str = join('', map { chr($_); } split(/\./, $source_ip));

# Read the binary trap data from STDIN or ARGV.  Normally this would
# come from the UDP receiver
my $d = join('', <>);

# Stuff my trap data into $ber
$ber->buffer($d);

print STDERR "Original packet:\n";
$ber->dump();

# Just decode the first two fields so we can tell what version we're dealing with
$ber->decode(
                SEQUENCE => [
                    INTEGER => \$version,
                    STRING => \$community,
                    BER => \$rest_of_trap,
                ],
) || die "Couldn't decode packet: ".$ber->error()."\n";

if ($version == 0) {
  #print STDERR "This is a version 1 trap, proceeding\n";

  # decode the PDU up to but not including the VARBINDS
  $rest_of_trap->decode(
    [ SEQUENCE => BER_CONTEXT | BER_CONSTRUCTOR | 0x04 ] =>
      [
        OBJECT_ID => \$enterprise_oid,
        [ STRING => BER_APPLICATION | 0x00 ] => \$agentaddr,
        INTEGER => \$generic,
        INTEGER => \$specific,
        [ INTEGER => BER_APPLICATION | 0x03 ] => \$timeticks,
        SEQUENCE => [ BER => \$varbind_ber, ],
      ],
  ) || die "Couldn't decode packet: ".$extra->error()."\n";;

  # now decode the actual VARBINDS (just the OIDs really, to decode the values
  # We'd have to go to the MIBs, which I neither want nor need to do
  my($r, $t_oid, $t_val, %varbinds);
  while ($r = $varbind_ber->decode(
    SEQUENCE => [
      OBJECT_ID => \$t_oid,
      ANY       => \$t_val,
    ], ))
  {
    if (!$r) {
      die "Couldn't decode SEQUENCE: ".$extra->error()."\n";
    }
    $varbinds{$t_oid} = $t_val;
  }

  if ($varbinds{$snmpTrapAddress} || $varbinds{"$snmpTrapAddress.0"}) {
    # the original trap already had the data, just print it back out
    print $d;
  } else {
    # snmpTrapAddress isn't present, create a new object and rebuild the packet
    my $new_trap = new Convert::BER;
    $new_trap->encode(
      SEQUENCE => [
        INTEGER => $version,
        STRING => $community,
        [ SEQUENCE => BER_CONTEXT | BER_CONSTRUCTOR | 0x04 ] =>
          [
            OBJECT_ID => $enterprise_oid,
            [ STRING => BER_APPLICATION | 0x00 ] => $agentaddr,
            INTEGER => $generic,
            INTEGER => $specific,
            [ INTEGER => BER_APPLICATION | 0x03 ] => $timeticks,
            SEQUENCE => [
              BER => $varbind_ber,
              # this next oid/val is the only mod we should be making
              SEQUENCE => [
                OBJECT_ID => "$snmpTrapAddress.0",
                [ STRING => BER_APPLICATION | 0x00 ] => $source_ip_str,
              ],
            ],
          ],
      ],
    );
    print STDERR "New packet:\n";
    $new_trap->dump();
    print $new_trap->buffer;
  }
} else {
  print STDERR "I don't know how to decode non-v1 packets yet\n";
  # send back the original packet
  print $d;  
}

So, that's it. Here's the kicker. I took ops at their word that they weren't getting the IP of the original sender in the trap. While working through this example, I found that, at least in the example they gave me, the original IP was in the agent-addr field in the trap. After showing them this and where in the API of the tool their using this is exposed they went off to try to make the change on their end. I'm daving the code above against the time they ask me for something I actually need to muck in the packet for, but for now the above will remain non-rigorously-tested proof of concept code. Hope it helps someone someday.

2
votes

Did you try NSNMP?

2
votes

Definitely check out SNMP_Session.

http://code.google.com/p/snmp-session/

Make sure to follow the links to the old distribution site which has several examples.

I've basically traveled the same path through Mon::SNMP, Convert::BER, TCP/IP Illustrated, etc.. SNMP_Session is the only thing I have been able to make work. By work, I mean accept a SNMP trap on UDP port 162 and decode it into string equivalents for logging without reinventing several wheels. I'm only using the receive trap functionality but I think it may do what you want too.

It's on Google Code, not CPAN though, so it's a little bit hard to find.