1use strict;
2package NSNMP::Agent;
3# Copyright (c) 2003-2004 AirWave Wireless, Inc.
4
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions
7# are met:
8
9#    1. Redistributions of source code must retain the above
10#    copyright notice, this list of conditions and the following
11#    disclaimer.
12#    2. Redistributions in binary form must reproduce the above
13#    copyright notice, this list of conditions and the following
14#    disclaimer in the documentation and/or other materials provided
15#    with the distribution.
16#    3. The name of the author may not be used to endorse or
17#    promote products derived from this software without specific
18#    prior written permission.
19
20# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
21# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
24# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
26# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31use NSNMP;
32use POSIX;
33use NSNMP::Mapper;
34
35# on my 500MHz machine, it handled 2800 requests per second when it
36# peremptorily returned an error message
37
38# adding the ability to actually return values slowed the error
39# messages to 2300 per second; returning messages with values in them
40# was roughly as fast
41
42# adding SNMP SET handling cut it down to 2200 messages per second or
43# so.
44
45# Adding community string handling cut it down to 2100 messages per
46# second or so.
47
48# Adding error handling and get-next handling cut it down to 2000
49# get-requests or 1900 get-next requests per second.  On a very small
50# dataset, though; it was down around 1300 for a slightly larger one.
51
52sub new {
53  my ($class, %args) = @_;
54  my $self = bless \%args, $class;
55  $self->{typemapper} = NSNMP::Mapper->new(%{$self->{types}});
56  $self->{_values} = {
57    map { NSNMP->encode_oid($_) => $self->{values}{$_} } keys %{$self->{values}}
58  };
59  $self->{_oids} = [sort keys %{$self->{_values}}]; # yay BER oids
60  $self->{lastoid_idx} = 0;
61  $self->{community} ||= 'public';
62  return $self;
63}
64
65sub _noSuchName {
66  my ($dr) = @_;
67  return NSNMP->encode(
68    request_id => $dr->request_id,
69    error_status => NSNMP::noSuchName,
70    error_index => 1, # XXX
71    type => NSNMP::GET_RESPONSE,
72    varbindlist => [$dr->varbindlist], # XXX?
73  );
74}
75
76sub _badValue {
77  my ($dr) = @_;
78  return NSNMP->encode(
79    request_id => $dr->request_id,
80    error_status => NSNMP::badValue,
81    error_index => 1, # XXX
82    type => NSNMP::GET_RESPONSE,
83    varbindlist => [$dr->varbindlist], # XXX??
84  );
85}
86
87sub next_oid_after {
88  my ($self, $oid) = @_;
89  my $oids = $self->{_oids};
90  my $lastindex = $self->{lastoid_idx};
91  my $lastoid = $oids->[$lastindex];
92  return $oids->[++$self->{lastoid_idx}] || NSNMP->encode_oid('.1.3')
93    if $lastoid and $oid eq $lastoid;
94  for my $ii (0..$#$oids) {
95    if ($oids->[$ii] gt $oid) {
96      $self->{lastoid_idx} = $ii;
97      return $oids->[$ii];
98    }
99  }
100  return NSNMP->encode_oid('.1.3');  # hope nobody tries to attach a value here
101}
102
103sub handle_get_request {
104  my ($self, $dr, $reqtype) = @_;
105  my @rvbl;
106  for my $varbind ($dr->varbindlist) {
107    my ($oid, $type, $value) = @{$varbind};
108    $oid = $self->next_oid_after($oid) if $reqtype eq NSNMP::GET_NEXT_REQUEST;
109    # XXX damn, I thought I could avoid decoding this OID:
110    my ($otype, $instance) = $self->{typemapper}->map(NSNMP->decode_oid($oid));
111    my $ovalue = $self->{_values}{$oid};
112    return _noSuchName($dr) if not defined $otype or not defined $ovalue;
113    push @rvbl, [$oid, $otype, $ovalue];
114  }
115  return NSNMP->encode(
116    request_id => $dr->request_id,
117    type => NSNMP::GET_RESPONSE,
118    varbindlist => \@rvbl,
119  );
120}
121
122sub handle_set_request {
123  my ($self, $dr) = @_;
124  for my $varbind ($dr->varbindlist) {
125    my ($oid, $type, $value) = @{$varbind};
126    my ($otype, $instance) = $self->{typemapper}->map(NSNMP->decode_oid($oid));
127    my $ovalue = $self->{_values}{$oid};
128    return _noSuchName($dr) if not defined $otype or not defined $ovalue;
129    return _badValue($dr) if $type ne $otype;
130    $self->{_values}{$oid} = $value;
131  }
132  return NSNMP->encode(
133    request_id => $dr->request_id,
134    type => NSNMP::GET_RESPONSE,
135    varbindlist => [$dr->varbindlist],
136  );
137}
138
139sub handle_request {
140  my ($self, $request) = @_;
141  my $dr = NSNMP->decode($request);
142  return undef unless $dr and $dr->community eq $self->{community};
143  my $type = $dr->type;
144  return(($type eq NSNMP::SET_REQUEST) ? handle_set_request($self, $dr) :
145	 handle_get_request($self, $dr, $type));
146}
147
148sub run {
149  my ($self, $socket) = @_;
150  my ($request, $requestor);
151  for (;;) {
152    if ($requestor = recv $socket, $request, 65536, 0) {
153      my $response = $self->handle_request($request, $requestor);
154      send $socket, $response, 0, $requestor if $response;
155    } else {
156      warn "Error on receive: $!";
157    }
158  }
159}
160
161# for testing purposes only
162# non-testing would require specifying host and returning errors sensibly
163sub spawn {
164  my ($self, $port) = @_;
165  # note we bind socket before forking, which has two advantages:
166  # - packets never get lost because they got sent before the child
167  #   binds the port
168  # - errors kill the main process, not the child.
169  my $listensocket = IO::Socket::INET->new(
170    Proto => 'udp',
171    LocalAddr => "127.0.0.1:$port",
172    ReuseAddr => 1,
173  );
174  die "Can't bind port $port: $!" unless $listensocket;
175  my $pid = fork();
176  die "Can't fork: $!" if not defined $pid;
177  if (not $pid) {
178    $self->run($listensocket);
179    POSIX::_exit(0);
180  }
181  $listensocket->close(); # in parent
182  return $pid;
183}
184
185# temp_agent --- test utility function for reaping agents later
186{
187  my $port = 16165;
188  my @pids;
189  sub temp_agent {
190    my ($self) = @_;
191    $port++;
192    my $pid = $self->spawn($port);
193    push @pids, $pid;
194    return "127.0.0.1:$port";
195  }
196  sub kill_temp_agents {
197    for (@pids) { kill 9, $_; wait() }
198    @pids = ();
199  }
200}
201
2021;
203