CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/snmp/manager.rb
Views: 11766
1
#
2
# Copyright (c) 2004 David R. Halliday
3
# All rights reserved.
4
#
5
# This SNMP library is free software. Redistribution is permitted under the
6
# same terms and conditions as the standard Ruby distribution. See the
7
# COPYING file in the Ruby distribution for details.
8
#
9
10
require 'snmp/pdu'
11
require 'snmp/mib'
12
require 'socket'
13
require 'timeout'
14
require 'thread'
15
16
module SNMP
17
18
class RequestTimeout < RuntimeError; end
19
20
##
21
# Wrap socket so that it can be easily substituted for testing or for
22
# using other transport types (e.g. TCP)
23
#
24
class UDPTransport
25
def initialize(socket = nil)
26
@socket = socket
27
28
if socket.nil?
29
@socket = UDPSocket.open
30
end
31
end
32
33
def close
34
@socket.close
35
end
36
37
def send(data, host, port)
38
@socket.send(data, 0, host, port)
39
end
40
41
def recv(max_bytes)
42
@socket.recv(max_bytes)
43
end
44
end
45
46
47
class RexUDPTransport
48
def initialize(socket = nil)
49
@socket = socket
50
51
if socket.nil?
52
@socket = UDPSocket.open
53
end
54
end
55
56
def close
57
@socket.close
58
end
59
60
def send(data, host, port, flags = 0)
61
begin
62
@socket.sendto(data, host, port, flags)
63
rescue NoMethodError
64
@socket.send(data, 0, host, port)
65
rescue ::Errno::EISCONN
66
@socket.write(data)
67
end
68
end
69
70
def recv(max_bytes)
71
@socket.recv(max_bytes)
72
end
73
end
74
75
76
##
77
# Manage a request-id in the range 1..2**31-1
78
#
79
class RequestId
80
MAX_REQUEST_ID = 2**31
81
82
def initialize
83
@lock = Mutex.new
84
@request_id = rand(MAX_REQUEST_ID)
85
end
86
87
def next
88
@lock.synchronize do
89
@request_id += 1
90
@request_id = 1 if @request_id == MAX_REQUEST_ID
91
return @request_id
92
end
93
end
94
95
def force_next(next_id)
96
new_request_id = next_id.to_i
97
if new_request_id < 1 || new_request_id >= MAX_REQUEST_ID
98
raise "Invalid request id: #{new_request_id}"
99
end
100
new_request_id = MAX_REQUEST_ID if new_request_id == 1
101
@lock.synchronize do
102
@request_id = new_request_id - 1
103
end
104
end
105
end
106
107
##
108
# == SNMP Manager
109
#
110
# This class provides a manager for interacting with a single SNMP agent.
111
#
112
# = Example
113
#
114
# require 'snmp'
115
#
116
# manager = SNMP::Manager.new(:Host => 'localhost', :Port => 1061)
117
# response = manager.get(["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"])
118
# response.each_varbind {|vb| puts vb.inspect}
119
# manager.close
120
#
121
# == Symbolic Object Names
122
#
123
# Symbolic names for SNMP object IDs can be used as parameters to the
124
# APIs in this class if the MIB modules are imported and the names of the
125
# MIBs are included in the MibModules configuration parameter.
126
#
127
# See MIB.varbind_list for a description of valid parameter formats.
128
#
129
# The following modules are loaded by default: "SNMPv2-SMI", "SNMPv2-MIB",
130
# "IF-MIB", "IP-MIB", "TCP-MIB", "UDP-MIB". All of the current IETF MIBs
131
# have been imported and are available for loading.
132
#
133
# Additional modules may be imported using the MIB class. The
134
# current implementation of the importing code requires that the
135
# external 'smidump' tool is available in your PATH. This tool can be
136
# obtained from the libsmi website at
137
# http://www.ibr.cs.tu-bs.de/projects/libsmi/ .
138
#
139
# = Example
140
#
141
# Do this once:
142
#
143
# SNMP::MIB.import_module(MY_MODULE_FILENAME, MIB_OUTPUT_DIR)
144
#
145
# Include your module in MibModules each time you create a Manager:
146
#
147
# SNMP::Manager.new(:Host => 'localhost', :MibDir => MIB_OUTPUT_DIR,
148
# :MibModules => ["MY-MODULE-MIB", "SNMPv2-MIB", ...])
149
#
150
151
class Manager
152
153
##
154
# Default configuration. Individual options may be overridden when
155
# the Manager is created.
156
#
157
DefaultConfig = {
158
:Host => 'localhost',
159
:Port => 161,
160
:TrapPort => 162,
161
:Socket => nil,
162
:Community => 'public',
163
:WriteCommunity => nil,
164
:Version => :SNMPv2c,
165
:Timeout => 1,
166
:Retries => 5,
167
:Transport => UDPTransport,
168
:MaxReceiveBytes => 8000,
169
:MibDir => MIB::DEFAULT_MIB_PATH,
170
:MibModules => ["SNMPv2-SMI", "SNMPv2-MIB", "IF-MIB", "IP-MIB", "TCP-MIB", "UDP-MIB"]}
171
172
@@request_id = RequestId.new
173
174
##
175
# Retrieves the current configuration of this Manager.
176
#
177
attr_reader :config
178
179
##
180
# Retrieves the MIB for this Manager.
181
#
182
attr_reader :mib
183
184
def initialize(config = {})
185
if block_given?
186
warn "SNMP::Manager::new() does not take block; use SNMP::Manager::open() instead"
187
end
188
@config = DefaultConfig.merge(config)
189
@config[:WriteCommunity] = @config[:WriteCommunity] || @config[:Community]
190
@host = @config[:Host]
191
@port = @config[:Port]
192
@socket = @config[:Socket]
193
@trap_port = @config[:TrapPort]
194
@community = @config[:Community]
195
@write_community = @config[:WriteCommunity]
196
@snmp_version = @config[:Version]
197
@timeout = @config[:Timeout]
198
@retries = @config[:Retries]
199
@transport = @config[:Transport].new(@socket)
200
@max_bytes = @config[:MaxReceiveBytes]
201
@mib = MIB.new
202
load_modules(@config[:MibModules], @config[:MibDir])
203
end
204
205
##
206
# Creates a Manager but also takes an optional block and automatically
207
# closes the transport connection used by this manager after the block
208
# completes.
209
#
210
def self.open(config = {})
211
manager = Manager.new(config)
212
if block_given?
213
begin
214
yield manager
215
ensure
216
manager.close
217
end
218
end
219
end
220
221
##
222
# Close the transport connection for this manager.
223
#
224
def close
225
@transport.close
226
end
227
228
def load_module(name)
229
@mib.load_module(name)
230
end
231
232
##
233
# Sends a get request for the supplied list of ObjectId or VarBind
234
# objects.
235
#
236
# Returns a Response PDU with the results of the request.
237
#
238
def get(object_list)
239
varbind_list = @mib.varbind_list(object_list, :NullValue)
240
request = GetRequest.new(@@request_id.next, varbind_list)
241
try_request(request)
242
end
243
244
##
245
# Sends a get request for the supplied list of ObjectId or VarBind
246
# objects.
247
#
248
# Returns a list of the varbind values only, not the entire response,
249
# in the same order as the initial object_list. This method is
250
# useful for retrieving scalar values.
251
#
252
# For example:
253
#
254
# SNMP::Manager.open(:Host => "localhost") do |manager|
255
# puts manager.get_value("sysDescr.0")
256
# end
257
#
258
def get_value(object_list)
259
if object_list.respond_to? :to_ary
260
get(object_list).vb_list.collect { |vb| vb.value }
261
else
262
get(object_list).vb_list.first.value
263
end
264
end
265
266
##
267
# Sends a get-next request for the supplied list of ObjectId or VarBind
268
# objects.
269
#
270
# Returns a Response PDU with the results of the request.
271
#
272
def get_next(object_list)
273
varbind_list = @mib.varbind_list(object_list, :NullValue)
274
request = GetNextRequest.new(@@request_id.next, varbind_list)
275
try_request(request)
276
end
277
278
##
279
# Sends a get-bulk request. The non_repeaters parameter specifies
280
# the number of objects in the object_list to be retrieved once. The
281
# remaining objects in the list will be retrieved up to the number of
282
# times specified by max_repetitions.
283
#
284
def get_bulk(non_repeaters, max_repetitions, object_list)
285
varbind_list = @mib.varbind_list(object_list, :NullValue)
286
request = GetBulkRequest.new(
287
@@request_id.next,
288
varbind_list,
289
non_repeaters,
290
max_repetitions)
291
try_request(request)
292
end
293
294
##
295
# Sends a set request using the supplied list of VarBind objects.
296
#
297
# Returns a Response PDU with the results of the request.
298
#
299
def set(object_list)
300
varbind_list = @mib.varbind_list(object_list, :KeepValue)
301
request = SetRequest.new(@@request_id.next, varbind_list)
302
try_request(request, @write_community)
303
end
304
305
##
306
# Sends an SNMPv1 style trap.
307
#
308
# enterprise: The enterprise OID from the IANA assigned numbers
309
# (http://www.iana.org/assignments/enterprise-numbers) as a String or
310
# an ObjectId.
311
#
312
# agent_addr: The IP address of the SNMP agent as a String or IpAddress.
313
#
314
# generic_trap: The generic trap identifier. One of :coldStart,
315
# :warmStart, :linkDown, :linkUp, :authenticationFailure,
316
# :egpNeighborLoss, or :enterpriseSpecific
317
#
318
# specific_trap: An integer representing the specific trap type for
319
# an enterprise-specific trap.
320
#
321
# timestamp: An integer representing the number of hundredths of
322
# a second that this system has been up.
323
#
324
# object_list: A list of additional varbinds to send with the trap.
325
#
326
# For example:
327
#
328
# Manager.open(:Version => :SNMPv1) do |snmp|
329
# snmp.trap_v1(
330
# "enterprises.9",
331
# "10.1.2.3",
332
# :enterpriseSpecific,
333
# 42,
334
# 12345,
335
# [VarBind.new("1.3.6.1.2.3.4", Integer.new(1))])
336
# end
337
#
338
def trap_v1(enterprise, agent_addr, generic_trap, specific_trap, timestamp, object_list=[])
339
vb_list = @mib.varbind_list(object_list, :KeepValue)
340
ent_oid = @mib.oid(enterprise)
341
agent_ip = IpAddress.new(agent_addr)
342
specific_int = Integer(specific_trap)
343
ticks = TimeTicks.new(timestamp)
344
trap = SNMPv1_Trap.new(ent_oid, agent_ip, generic_trap, specific_int, ticks, vb_list)
345
send_request(trap, @community, @host, @trap_port)
346
end
347
348
##
349
# Sends an SNMPv2c style trap.
350
#
351
# sys_up_time: An integer representing the number of hundredths of
352
# a second that this system has been up.
353
#
354
# trap_oid: An ObjectId or String with the OID identifier for this
355
# trap.
356
#
357
# object_list: A list of additional varbinds to send with the trap.
358
#
359
def trap_v2(sys_up_time, trap_oid, object_list=[])
360
vb_list = create_trap_vb_list(sys_up_time, trap_oid, object_list)
361
trap = SNMPv2_Trap.new(@@request_id.next, vb_list)
362
send_request(trap, @community, @host, @trap_port)
363
end
364
365
##
366
# Sends an inform request using the supplied varbind list.
367
#
368
# sys_up_time: An integer representing the number of hundredths of
369
# a second that this system has been up.
370
#
371
# trap_oid: An ObjectId or String with the OID identifier for this
372
# inform request.
373
#
374
# object_list: A list of additional varbinds to send with the inform.
375
#
376
def inform(sys_up_time, trap_oid, object_list=[])
377
vb_list = create_trap_vb_list(sys_up_time, trap_oid, object_list)
378
request = InformRequest.new(@@request_id.next, vb_list)
379
try_request(request, @community, @host, @trap_port)
380
end
381
382
##
383
# Helper method for building VarBindList for trap and inform requests.
384
#
385
def create_trap_vb_list(sys_up_time, trap_oid, object_list)
386
vb_args = @mib.varbind_list(object_list, :KeepValue)
387
uptime_vb = VarBind.new(SNMP::SYS_UP_TIME_OID, TimeTicks.new(sys_up_time.to_int))
388
trap_vb = VarBind.new(SNMP::SNMP_TRAP_OID_OID, @mib.oid(trap_oid))
389
VarBindList.new([uptime_vb, trap_vb, *vb_args])
390
end
391
392
##
393
# Walks a list of ObjectId or VarBind objects using get_next until
394
# the response to the first OID in the list reaches the end of its
395
# MIB subtree.
396
#
397
# The varbinds from each get_next are yielded to the given block as
398
# they are retrieved. The result is yielded as a VarBind when walking
399
# a single object or as a VarBindList when walking a list of objects.
400
#
401
# Normally this method is used for walking tables by providing an
402
# ObjectId for each column of the table.
403
#
404
# For example:
405
#
406
# SNMP::Manager.open(:Host => "localhost") do |manager|
407
# manager.walk("ifTable") { |vb| puts vb }
408
# end
409
#
410
# SNMP::Manager.open(:Host => "localhost") do |manager|
411
# manager.walk(["ifIndex", "ifDescr"]) do |index, descr|
412
# puts "#{index.value} #{descr.value}"
413
# end
414
# end
415
#
416
# The index_column identifies the column that will provide the index
417
# for each row. This information is used to deal with "holes" in a
418
# table (when a row is missing a varbind for one column). A missing
419
# varbind is replaced with a varbind with the value NoSuchInstance.
420
#
421
# Note: If you are getting back rows where all columns have a value of
422
# NoSuchInstance then your index column is probably missing one of the
423
# rows. Choose an index column that includes all indexes for the table.
424
#
425
def walk(object_list, index_column=0)
426
raise ArgumentError, "expected a block to be given" unless block_given?
427
vb_list = @mib.varbind_list(object_list, :NullValue)
428
raise ArgumentError, "index_column is past end of varbind list" if index_column >= vb_list.length
429
is_single_vb = object_list.respond_to?(:to_str) ||
430
object_list.respond_to?(:to_varbind)
431
start_list = vb_list
432
start_oid = vb_list[index_column].name
433
last_oid = start_oid
434
loop do
435
vb_list = get_next(vb_list).vb_list
436
index_vb = vb_list[index_column]
437
break if EndOfMibView == index_vb.value
438
stop_oid = index_vb.name
439
if stop_oid <= last_oid
440
# warn "OIDs are not increasing, #{last_oid} followed by #{stop_oid}"
441
break
442
end
443
break unless stop_oid.subtree_of?(start_oid)
444
last_oid = stop_oid
445
if is_single_vb
446
yield index_vb
447
else
448
vb_list = validate_row(vb_list, start_list, index_column)
449
yield vb_list
450
end
451
end
452
end
453
454
##
455
# Helper method for walk. Checks all of the VarBinds in vb_list to
456
# make sure that the row indices match. If the row index does not
457
# match the index column, then that varbind is replaced with a varbind
458
# with a value of NoSuchInstance.
459
#
460
def validate_row(vb_list, start_list, index_column)
461
start_vb = start_list[index_column]
462
index_vb = vb_list[index_column]
463
row_index = index_vb.name.index(start_vb.name)
464
vb_list.each_index do |i|
465
if i != index_column
466
expected_oid = start_list[i].name + row_index
467
if vb_list[i].name != expected_oid
468
vb_list[i] = VarBind.new(expected_oid, NoSuchInstance)
469
end
470
end
471
end
472
vb_list
473
end
474
private :validate_row
475
476
##
477
# Set the next request-id instead of letting it be generated
478
# automatically. This method is useful for testing and debugging.
479
#
480
def next_request_id=(request_id)
481
@@request_id.force_next(request_id)
482
end
483
484
private
485
486
def warn(message)
487
trace = caller(2)
488
location = trace[0].sub(/:in.*/,'')
489
Kernel::warn "#{location}: warning: #{message}"
490
end
491
492
def load_modules(module_list, mib_dir)
493
module_list.each { |m| @mib.load_module(m, mib_dir) }
494
end
495
496
def try_request(request, community=@community, host=@host, port=@port)
497
(@retries.to_i + 1).times do |n|
498
send_request(request, community, host, port)
499
begin
500
Timeout.timeout(@timeout) do
501
return get_response(request)
502
end
503
rescue Timeout::Error
504
# no action - try again
505
end
506
end
507
raise RequestTimeout, "host #{@config[:Host]} not responding", caller
508
end
509
510
def send_request(request, community, host, port)
511
message = Message.new(@snmp_version, community, request)
512
@transport.send(message.encode, host, port)
513
end
514
515
##
516
# Wait until response arrives. Ignore responses with mismatched IDs;
517
# these responses are typically from previous requests that timed out
518
# or almost timed out.
519
#
520
def get_response(request)
521
begin
522
data = @transport.recv(@max_bytes)
523
message = Message.decode(data)
524
response = message.pdu
525
end until request.request_id == response.request_id
526
response
527
end
528
end
529
530
class UDPServerTransport
531
def initialize(host, port)
532
@socket = UDPSocket.open
533
@socket.bind(host, port)
534
end
535
536
def close
537
@socket.close
538
end
539
540
def send(data, host, port)
541
@socket.send(data, 0, host, port)
542
end
543
544
def recvfrom(max_bytes)
545
data, host_info = @socket.recvfrom(max_bytes)
546
flags, host_port, host_name, host_ip = host_info
547
return data, host_ip, host_port
548
end
549
end
550
551
##
552
# == SNMP Trap Listener
553
#
554
# Listens to a socket and processes received traps and informs in a separate
555
# thread.
556
#
557
# === Example
558
#
559
# require 'snmp'
560
#
561
# m = SNMP::TrapListener.new(:Port => 1062, :Community => 'public') do |manager|
562
# manager.on_trap_default { |trap| p trap }
563
# end
564
# m.join
565
#
566
class TrapListener
567
DefaultConfig = {
568
:Host => 'localhost',
569
:Port => 162,
570
:Community => 'public',
571
:ServerTransport => UDPServerTransport,
572
:MaxReceiveBytes => 8000}
573
574
NULL_HANDLER = Proc.new {}
575
576
##
577
# Start a trap handler thread. If a block is provided then the block
578
# is executed before trap handling begins. This block is typically used
579
# to define the trap handler blocks.
580
#
581
# The trap handler blocks execute in the context of the trap handler thread.
582
#
583
# The most specific trap handler is executed when a trap arrives. Only one
584
# handler is executed. The handlers are checked in the following order:
585
#
586
# 1. handler for a specific OID
587
# 2. handler for a specific SNMP version
588
# 3. default handler
589
#
590
def initialize(config={}, &block)
591
@config = DefaultConfig.dup.update(config)
592
@transport = @config[:ServerTransport].new(@config[:Host], @config[:Port])
593
@max_bytes = @config[:MaxReceiveBytes]
594
@handler_init = block
595
@oid_handler = {}
596
@v1_handler = nil
597
@v2c_handler = nil
598
@default_handler = nil
599
@lock = Mutex.new
600
@handler_thread = Thread.new(self) { |m| process_traps(m) }
601
end
602
603
##
604
# Define the default trap handler. The default trap handler block is
605
# executed only if no other block is applicable. This handler should
606
# expect to receive both SNMPv1_Trap and SNMPv2_Trap objects.
607
#
608
def on_trap_default(&block)
609
raise ArgumentError, "a block must be provided" unless block
610
@lock.synchronize { @default_handler = block }
611
end
612
613
##
614
# Define a trap handler block for a specific trap ObjectId. This handler
615
# only applies to SNMPv2 traps. Note that symbolic OIDs are not
616
# supported by this method (like in the SNMP.Manager class).
617
#
618
def on_trap(object_id, &block)
619
raise ArgumentError, "a block must be provided" unless block
620
@lock.synchronize { @oid_handler[ObjectId.new(object_id)] = block }
621
end
622
623
##
624
# Define a trap handler block for all SNMPv1 traps. The trap yielded
625
# to the block will always be an SNMPv1_Trap.
626
#
627
def on_trap_v1(&block)
628
raise ArgumentError, "a block must be provided" unless block
629
@lock.synchronize { @v1_handler = block }
630
end
631
632
##
633
# Define a trap handler block for all SNMPv2c traps. The trap yielded
634
# to the block will always be an SNMPv2_Trap. Note that InformRequest
635
# is a subclass of SNMPv2_Trap, so inform PDUs are also received by
636
# this handler.
637
#
638
def on_trap_v2c(&block)
639
raise ArgumentError, "a block must be provided" unless block
640
@lock.synchronize { @v2c_handler = block }
641
end
642
643
##
644
# Joins the current thread to the trap handler thread.
645
#
646
# See also Thread#join.
647
#
648
def join
649
@handler_thread.join
650
end
651
652
##
653
# Stops the trap handler thread and releases the socket.
654
#
655
# See also Thread#exit.
656
#
657
def exit
658
@handler_thread.exit
659
@transport.close
660
end
661
662
alias kill exit
663
alias terminate exit
664
665
private
666
667
def process_traps(trap_listener)
668
@handler_init.call(trap_listener) if @handler_init
669
loop do
670
data, source_ip, source_port = @transport.recvfrom(@max_bytes)
671
begin
672
message = Message.decode(data)
673
if @config[:Community] == message.community
674
trap = message.pdu
675
if trap.kind_of?(InformRequest)
676
@transport.send(message.response.encode, source_ip, source_port)
677
end
678
trap.source_ip = source_ip
679
select_handler(trap).call(trap)
680
end
681
rescue => e
682
puts "Error handling trap: #{e}"
683
puts e.backtrace.join("\n")
684
puts "Received data:"
685
p data
686
end
687
end
688
end
689
690
def select_handler(trap)
691
@lock.synchronize do
692
if trap.kind_of?(SNMPv2_Trap)
693
oid = trap.trap_oid
694
if @oid_handler[oid]
695
return @oid_handler[oid]
696
elsif @v2c_handler
697
return @v2c_handler
698
elsif @default_handler
699
return @default_handler
700
else
701
return NULL_HANDLER
702
end
703
elsif trap.kind_of?(SNMPv1_Trap)
704
if @v1_handler
705
return @v1_handler
706
elsif @default_handler
707
return @default_handler
708
else
709
return NULL_HANDLER
710
end
711
else
712
return NULL_HANDLER
713
end
714
end
715
end
716
end
717
718
end
719
720