netsnmp 0.2.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6bb62b9abd416188660e04be1c12cf009b1df0e307030b9c239ea4b2acd73cc
4
- data.tar.gz: ee88f7eb292f1a0077ab25e4677713290447e0e7755049bbe6db50f07d15d6a1
3
+ metadata.gz: 5119f7e6ec751a4d3ba54076e5597b0667de904eda86802e5e3544e40a753172
4
+ data.tar.gz: 35924abe6afc24d8a41c8b6429a0566644d6bf4674b2b83704ac3f215e217487
5
5
  SHA512:
6
- metadata.gz: fc90cc626d4be2a04d598ec267bcfc108e87aea8e8a585a7611033fc56cb6c174a222250bb9eeeb05b2d03d058983b106c92c30ba3d91e922b171ecfbc37041a
7
- data.tar.gz: '09dbbb277ae24d9b760c0de70738a02037da527d653d5e003632c1b41d808cc21c44034fb51bec63394a132fe3c30ffaac1adc846dc061ca483e13fc0719af97'
6
+ metadata.gz: 7662ff27d3242840034b7e72f0a85132fa7007ef44ab4e207ac79f13cfdc29c3709884b098bba97d83e0f157618a8ca8c4dab5d8919bc6686aba4452b990213f
7
+ data.tar.gz: 673c3ce56ca0154f5efc6247204ffebbd9d0966d43ba4371ba2cee98d10f1496e579eef84acf24349109315d35febb7c42716a8b88c432200cee3eeddd63cca3
data/README.md CHANGED
@@ -32,7 +32,7 @@ This gem provides:
32
32
 
33
33
  * Implementation in ruby of the SNMP Protocol for v3, v2c and v1 (most notable the rfc3414 and 3826).
34
34
  * Client/Manager API with simple interface for get, genext, set and walk.
35
- * No dependencies.
35
+ * Pure Ruby.
36
36
  * Support for concurrency and evented I/O.
37
37
 
38
38
  ## Why?
@@ -54,10 +54,12 @@ All of these issues are resolved here.
54
54
  ## Features
55
55
 
56
56
  * Client Interface, which supports SNMP v3, v2c, and v1
57
- * Supports get, getnext, set and walk calls.
57
+ * Supports get, getnext, set and walk calls
58
+ * MIB support
58
59
  * Proxy IO object support (for eventmachine/celluloid-io)
59
60
  * Ruby >= 2.1 support (modern)
60
61
  * Pure Ruby (no FFI)
62
+ * Easy PDU debugging
61
63
 
62
64
  ## Examples
63
65
 
@@ -73,12 +75,11 @@ manager = NETSNMP::Client.new(host: "localhost", port: 33445, username: "simulat
73
75
  context: "a172334d7d97871b72241397f713fa12")
74
76
 
75
77
  # SNMP get
76
- # sysName.0
77
- manager.get(oid: "1.3.6.1.2.1.1.0") #=> 'tt'
78
+ manager.get(oid: "sysName.0") #=> 'tt'
78
79
 
79
80
  # SNMP walk
80
81
  # sysORDescr
81
- manager.walk(oid: "1.3.6.1.2.1.1.1").each do |oid_code, value|
82
+ manager.walk(oid: "sysORDescr").each do |oid_code, value|
82
83
  # do something with them
83
84
  puts "for #{oid_code}: #{value}"
84
85
  end
@@ -135,6 +136,29 @@ manager.set("somecounteroid", value: 999999, type: 6)
135
136
  ```
136
137
  * Fork this library, extend support, write a test and submit a PR (the desired solution ;) )
137
138
 
139
+ ## MIB
140
+
141
+ `netsnmp` will load the default MIBs from known or advertised (via `MIBDIRS`) directories (provided that they're installed in the system). These will be used for the OID conversion.
142
+
143
+ Sometimes you'll need to load more, your own MIBs, in which case, you can use the following API:
144
+
145
+ ```ruby
146
+ require "netsnmp"
147
+
148
+ NETSNMP::MIB.load("MY-MIB")
149
+ # or, if it's not in any of the known locations
150
+ NETSNMP::MIB.load("/path/to/MY-MIB.txt")
151
+ ```
152
+
153
+ You can install common SNMP mibs by using your package manager:
154
+
155
+ ```
156
+ # using apt-get
157
+ > apt-get install snmp-mibs-downloader
158
+ # using apk
159
+ > apk --update add net-snmp-libs
160
+ ```
161
+
138
162
  ## Concurrency
139
163
 
140
164
  In ruby, you are usually adviced not to share IO objects across threads. The same principle applies here to `NETSNMP::Client`: provided you use it within a thread of execution, it should behave safely. So, something like this would be possible:
@@ -213,10 +237,26 @@ NETSNMP::Client.new(share_options.merge(proxy: router_proxy, security_parameters
213
237
  end
214
238
  ```
215
239
 
240
+ ## Compatibility
241
+
242
+ This library supports and is tested against ruby versions 2.1 or more recent, including ruby 3. It also supports and tests against Truffleruby.
243
+
216
244
  ## OpenSSL
217
245
 
218
246
  All encoding/decoding/encryption/decryption/digests are done using `openssl`, which is (still) a part of the standard library. If at some point `openssl` is removed and not specifically distributed, you'll have to install it yourself. Hopefully this will never happen.
219
247
 
248
+ It also uses the `openssl` ASN.1 API to encode/decode BERs, which is known to be strict, and [may not be able to decode PDUs if not compliant with the supported RFC](https://github.com/swisscom/ruby-netsnmp/issues/47).
249
+
250
+ ## Debugging
251
+
252
+ You can either set the `NETSNMP_DEBUG` to the desided debug level (currently, 1 and 2). The logs will be written to stderr.
253
+
254
+ You can also set it for a specific client:
255
+
256
+ ```ruby
257
+ manager2 = NETSNMP::Client.new(debug: $stderr, debug_level: 2, ....)
258
+ ```
259
+
220
260
 
221
261
  ## Tests
222
262
 
@@ -272,7 +312,6 @@ The job of the CI is:
272
312
 
273
313
  There are some features which this gem doesn't support. It was built to provide a client (or manager, in SNMP language) implementation only, and the requirements were fulfilled. However, these notable misses will stand-out:
274
314
 
275
- * No MIB support (you can only work with OIDs)
276
315
  * No server (Agent, in SNMP-ish) implementation.
277
316
  * No getbulk support.
278
317
 
data/lib/netsnmp.rb CHANGED
@@ -33,46 +33,16 @@ rescue LoadError
33
33
  end
34
34
  end
35
35
 
36
- module NETSNMP
37
- module StringExtensions
38
- # If you wonder why this is there: the oauth feature uses a refinement to enhance the
39
- # Regexp class locally with #match? , but this is never tested, because ActiveSupport
40
- # monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
41
- # :nocov:
42
- refine(String) do
43
- def match?(*args)
44
- !match(*args).nil?
45
- end
46
- end
47
- end
48
-
49
- def self.debug=(io)
50
- @debug_output = io
51
- end
52
-
53
- def self.debug(&blk)
54
- @debug_output << blk.call + "\n" if @debug_output
55
- end
56
-
57
- unless defined?(Hexdump) # support the hexdump gem
58
- module Hexdump
59
- def self.dump(data, width: 8)
60
- pairs = data.unpack("H*").first.scan(/.{4}/)
61
- pairs.each_slice(width).map do |row|
62
- row.join(" ")
63
- end.join("\n")
64
- end
65
- end
66
- end
67
- end
68
-
69
36
  require "netsnmp/errors"
37
+ require "netsnmp/extensions"
38
+ require "netsnmp/loggable"
70
39
 
71
40
  require "netsnmp/timeticks"
72
41
 
73
42
  require "netsnmp/oid"
74
43
  require "netsnmp/varbind"
75
44
  require "netsnmp/pdu"
45
+ require "netsnmp/mib"
76
46
  require "netsnmp/session"
77
47
 
78
48
  require "netsnmp/scoped_pdu"
@@ -77,7 +77,7 @@ module NETSNMP
77
77
  # @return [Enumerator] the enumerator-collection of the oid-value pairs
78
78
  #
79
79
  def walk(oid:)
80
- walkoid = oid
80
+ walkoid = OID.build(oid)
81
81
  Enumerator.new do |y|
82
82
  code = walkoid
83
83
  first_response_code = nil
@@ -153,7 +153,7 @@ module NETSNMP
153
153
  retries = @retries
154
154
  begin
155
155
  yield
156
- rescue Timeout::Error => e
156
+ rescue Timeout::Error, IdNotInTimeWindowError => e
157
157
  raise e if retries.zero?
158
158
  retries -= 1
159
159
  retry
@@ -22,13 +22,12 @@ module NETSNMP
22
22
  end
23
23
 
24
24
  encrypted_data = cipher.update(decrypted_data) + cipher.final
25
- NETSNMP.debug { "encrypted:\n#{Hexdump.dump(encrypted_data)}" }
26
25
 
27
26
  [encrypted_data, salt]
28
27
  end
29
28
 
30
29
  def decrypt(encrypted_data, salt:, engine_boots:, engine_time:)
31
- raise Error, "invalid priv salt received" unless (salt.length % 8).zero?
30
+ raise Error, "invalid priv salt received" unless !salt.empty? && (salt.length % 8).zero?
32
31
 
33
32
  cipher = OpenSSL::Cipher::AES128.new(:CFB)
34
33
  cipher.padding = 0
@@ -39,7 +38,6 @@ module NETSNMP
39
38
  cipher.key = aes_key
40
39
  cipher.iv = iv
41
40
  decrypted_data = cipher.update(encrypted_data) + cipher.final
42
- NETSNMP.debug { "decrypted:\n#{Hexdump.dump(decrypted_data)}" }
43
41
 
44
42
  hlen, bodylen = OpenSSL::ASN1.traverse(decrypted_data) { |_, _, x, y, *| break x, y }
45
43
  decrypted_data.byteslice(0, hlen + bodylen)
@@ -24,7 +24,6 @@ module NETSNMP
24
24
  end
25
25
 
26
26
  encrypted_data = cipher.update(decrypted_data) + cipher.final
27
- NETSNMP.debug { "encrypted:\n#{Hexdump.dump(encrypted_data)}" }
28
27
  [encrypted_data, salt]
29
28
  end
30
29
 
@@ -41,7 +40,6 @@ module NETSNMP
41
40
  cipher.key = des_key
42
41
  cipher.iv = iv
43
42
  decrypted_data = cipher.update(encrypted_data) + cipher.final
44
- NETSNMP.debug { "decrypted:\n#{Hexdump.dump(decrypted_data)}" }
45
43
 
46
44
  hlen, bodylen = OpenSSL::ASN1.traverse(decrypted_data) { |_, _, x, y, *| break x, y }
47
45
  decrypted_data.byteslice(0, hlen + bodylen)
@@ -4,4 +4,5 @@ module NETSNMP
4
4
  Error = Class.new(StandardError)
5
5
  ConnectionFailed = Class.new(Error)
6
6
  AuthenticationFailed = Class.new(Error)
7
+ IdNotInTimeWindowError = Class.new(Error)
7
8
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NETSNMP
4
+ module IsNumericExtensions
5
+ refine String do
6
+ def integer?
7
+ each_byte do |byte|
8
+ return false unless byte >= 48 && byte <= 57
9
+ end
10
+ true
11
+ end
12
+ end
13
+ end
14
+
15
+ module StringExtensions
16
+ refine(String) do
17
+ unless String.method_defined?(:match?)
18
+ def match?(*args)
19
+ !match(*args).nil?
20
+ end
21
+ end
22
+
23
+ unless String.method_defined?(:unpack1)
24
+ def unpack1(format)
25
+ unpack(format).first
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ module ASNExtensions
32
+ ASN_COLORS = {
33
+ OpenSSL::ASN1::Sequence => 34, # blue
34
+ OpenSSL::ASN1::OctetString => 32, # green
35
+ OpenSSL::ASN1::Integer => 33, # yellow
36
+ OpenSSL::ASN1::ObjectId => 35, # magenta
37
+ OpenSSL::ASN1::ASN1Data => 36 # cyan
38
+ }.freeze
39
+
40
+ # basic types
41
+ ASN_COLORS.each_key do |klass|
42
+ refine(klass) do
43
+ def to_hex
44
+ "#{colorize_hex} (#{value.to_s.inspect})"
45
+ end
46
+ end
47
+ end
48
+
49
+ # composite types
50
+ refine(OpenSSL::ASN1::Sequence) do
51
+ def to_hex
52
+ values = value.map(&:to_der).join
53
+ hex_values = value.map(&:to_hex).map { |s| s.gsub(/(\t+)/) { "\t#{Regexp.last_match(1)}" } }.map { |s| "\n\t#{s}" }.join
54
+ der = to_der
55
+ der = der.sub(values, "")
56
+
57
+ "#{colorize_hex(der)}#{hex_values}"
58
+ end
59
+ end
60
+
61
+ refine(OpenSSL::ASN1::ASN1Data) do
62
+ attr_reader :label
63
+
64
+ def with_label(label)
65
+ @label = label
66
+ self
67
+ end
68
+
69
+ def to_hex
70
+ case value
71
+ when Array
72
+ values = value.map(&:to_der).join
73
+ hex_values = value.map(&:to_hex)
74
+ .map { |s| s.gsub(/(\t+)/) { "\t#{Regexp.last_match(1)}" } }
75
+ .map { |s| "\n\t#{s}" }.join
76
+ der = to_der
77
+ der = der.sub(values, "")
78
+ else
79
+ der = to_der
80
+ hex_values = nil
81
+ end
82
+
83
+ "#{colorize_hex(der)}#{hex_values}"
84
+ end
85
+
86
+ private
87
+
88
+ def colorize_hex(der = to_der)
89
+ hex = Hexdump.dump(der, separator: " ")
90
+ lbl = @label || self.class.name.split("::").last
91
+ "#{lbl}: \e[#{ASN_COLORS[self.class]}m#{hex}\e[0m"
92
+ end
93
+ end
94
+ end
95
+
96
+ module Hexdump
97
+ using StringExtensions
98
+
99
+ def self.dump(data, width: 8, in_groups_of: 4, separator: "\n")
100
+ pairs = data.unpack1("H*").scan(/.{#{in_groups_of}}/)
101
+ pairs.each_slice(width).map do |row|
102
+ row.join(" ")
103
+ end.join(separator)
104
+ end
105
+ end
106
+
107
+ # Like a string, but it prints an hex-string version of itself
108
+ class HexString < String
109
+ def inspect
110
+ Hexdump.dump(self, in_groups_of: 2, separator: " ")
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NETSNMP
4
+ module Loggable
5
+ DEBUG = ENV.key?("NETSNMP_DEBUG") ? $stderr : nil
6
+ DEBUG_LEVEL = (ENV["NETSNMP_DEBUG"] || 1).to_i
7
+
8
+ def initialize(debug: DEBUG, debug_level: DEBUG_LEVEL, **opts)
9
+ super(**opts)
10
+ @debug = debug
11
+ @debug_level = debug_level
12
+ end
13
+
14
+ private
15
+
16
+ COLORS = {
17
+ black: 30,
18
+ red: 31,
19
+ green: 32,
20
+ yellow: 33,
21
+ blue: 34,
22
+ magenta: 35,
23
+ cyan: 36,
24
+ white: 37
25
+ }.freeze
26
+
27
+ def log(level: @debug_level)
28
+ return unless @debug
29
+ return unless @debug_level >= level
30
+
31
+ debug_stream = @debug
32
+
33
+ debug_stream << (+"\n" << yield << "\n")
34
+ end
35
+ end
36
+ end
@@ -2,41 +2,76 @@
2
2
 
3
3
  module NETSNMP
4
4
  # Factory for the SNMP v3 Message format
5
- module Message
6
- module_function
5
+ class Message
6
+ using ASNExtensions
7
7
 
8
- AUTHNONE = OpenSSL::ASN1::OctetString.new("\x00" * 12)
8
+ prepend Loggable
9
+
10
+ AUTHNONE = OpenSSL::ASN1::OctetString.new("\x00" * 12).with_label(:auth_mask)
9
11
  PRIVNONE = OpenSSL::ASN1::OctetString.new("")
10
- MSG_MAX_SIZE = OpenSSL::ASN1::Integer.new(65507)
11
- MSG_SECURITY_MODEL = OpenSSL::ASN1::Integer.new(3) # usmSecurityModel
12
- MSG_VERSION = OpenSSL::ASN1::Integer.new(3)
12
+ MSG_MAX_SIZE = OpenSSL::ASN1::Integer.new(65507).with_label(:max_message_size)
13
+ MSG_SECURITY_MODEL = OpenSSL::ASN1::Integer.new(3).with_label(:security_model) # usmSecurityModel
14
+ MSG_VERSION = OpenSSL::ASN1::Integer.new(3).with_label(:message_version)
13
15
  MSG_REPORTABLE = 4
14
16
 
17
+ def initialize(**); end
18
+
19
+ def verify(stream, auth_param, security_level, security_parameters:)
20
+ security_parameters.verify(stream.sub(auth_param, AUTHNONE.value), auth_param, security_level: security_level)
21
+ end
22
+
15
23
  # @param [String] payload of an snmp v3 message which can be decoded
16
24
  # @param [NETSMP::SecurityParameters, #decode] security_parameters knowns how to decode the stream
17
25
  #
18
26
  # @return [NETSNMP::ScopedPDU] the decoded PDU
19
27
  #
20
28
  def decode(stream, security_parameters:)
21
- asn_tree = OpenSSL::ASN1.decode(stream)
22
- _version, _headers, sec_params, pdu_payload = asn_tree.value
29
+ log { "received encoded V3 message" }
30
+ log { Hexdump.dump(stream) }
31
+ asn_tree = OpenSSL::ASN1.decode(stream).with_label(:v3_message)
32
+
33
+ version, headers, sec_params, pdu_payload = asn_tree.value
34
+ version.with_label(:message_version)
35
+ headers.with_label(:headers)
36
+ sec_params.with_label(:security_params)
37
+ pdu_payload.with_label(:pdu)
38
+
39
+ _, _, message_flags, = headers.value
23
40
 
24
- sec_params_asn = OpenSSL::ASN1.decode(sec_params.value).value
41
+ # get last byte
42
+ # discard the left-outermost bits and keep the remaining two
43
+ security_level = message_flags.with_label(:message_flags).value.unpack("C*").last & 3
25
44
 
26
- engine_id, engine_boots, engine_time, _username, auth_param, priv_param = sec_params_asn.map(&:value)
45
+ sec_params_asn = OpenSSL::ASN1.decode(sec_params.value).with_label(:security_params)
27
46
 
28
- # validate_authentication
29
- security_parameters.verify(stream.sub(auth_param, AUTHNONE.value), auth_param)
47
+ engine_id, engine_boots, engine_time, username, auth_param, priv_param = sec_params_asn.value
48
+ engine_id.with_label(:engine_id)
49
+ engine_boots.with_label(:engine_boots)
50
+ engine_time.with_label(:engine_time)
51
+ username.with_label(:username)
52
+ auth_param.with_label(:auth_param)
53
+ priv_param.with_label(:priv_param)
30
54
 
31
- engine_boots = engine_boots.to_i
32
- engine_time = engine_time.to_i
55
+ log(level: 2) { asn_tree.to_hex }
56
+ log(level: 2) { sec_params_asn.to_hex }
33
57
 
34
- encoded_pdu = security_parameters.decode(pdu_payload, salt: priv_param,
58
+ auth_param = auth_param.value
59
+
60
+ engine_boots = engine_boots.value.to_i
61
+ engine_time = engine_time.value.to_i
62
+
63
+ encoded_pdu = security_parameters.decode(pdu_payload, salt: priv_param.value,
35
64
  engine_boots: engine_boots,
36
- engine_time: engine_time)
65
+ engine_time: engine_time,
66
+ security_level: security_level)
37
67
 
68
+ log { "received response PDU" }
38
69
  pdu = ScopedPDU.decode(encoded_pdu)
39
- [pdu, engine_id, engine_boots, engine_time]
70
+ pdu.auth_param = auth_param
71
+ pdu.security_level = security_level
72
+
73
+ log(level: 2) { pdu.to_hex }
74
+ [pdu, engine_id.value, engine_boots, engine_time]
40
75
  end
41
76
 
42
77
  # @param [NETSNMP::ScopedPDU] the PDU to encode in the message
@@ -45,36 +80,49 @@ module NETSNMP
45
80
  # @return [String] the byte representation of an SNMP v3 Message
46
81
  #
47
82
  def encode(pdu, security_parameters:, engine_boots: 0, engine_time: 0)
83
+ log(level: 2) { pdu.to_hex }
84
+ log { "encoding PDU in V3 message..." }
48
85
  scoped_pdu, salt_param = security_parameters.encode(pdu, salt: PRIVNONE,
49
86
  engine_boots: engine_boots,
50
87
  engine_time: engine_time)
51
88
 
52
89
  sec_params = OpenSSL::ASN1::Sequence.new([
53
- OpenSSL::ASN1::OctetString.new(security_parameters.engine_id),
54
- OpenSSL::ASN1::Integer.new(engine_boots),
55
- OpenSSL::ASN1::Integer.new(engine_time),
56
- OpenSSL::ASN1::OctetString.new(security_parameters.username),
90
+ OpenSSL::ASN1::OctetString.new(security_parameters.engine_id).with_label(:engine_id),
91
+ OpenSSL::ASN1::Integer.new(engine_boots).with_label(:engine_boots),
92
+ OpenSSL::ASN1::Integer.new(engine_time).with_label(:engine_time),
93
+ OpenSSL::ASN1::OctetString.new(security_parameters.username).with_label(:username),
57
94
  AUTHNONE,
58
95
  salt_param
59
- ])
96
+ ]).with_label(:security_params)
97
+ log(level: 2) { sec_params.to_hex }
98
+
60
99
  message_flags = MSG_REPORTABLE | security_parameters.security_level
61
- message_id = OpenSSL::ASN1::Integer.new(SecureRandom.random_number(2147483647))
100
+ message_id = OpenSSL::ASN1::Integer.new(SecureRandom.random_number(2147483647)).with_label(:message_id)
62
101
  headers = OpenSSL::ASN1::Sequence.new([
63
- message_id, MSG_MAX_SIZE,
64
- OpenSSL::ASN1::OctetString.new([String(message_flags)].pack("h*")),
102
+ message_id,
103
+ MSG_MAX_SIZE,
104
+ OpenSSL::ASN1::OctetString.new([String(message_flags)].pack("h*")).with_label(:message_flags),
65
105
  MSG_SECURITY_MODEL
66
- ])
106
+ ]).with_label(:headers)
67
107
 
68
108
  encoded = OpenSSL::ASN1::Sequence([
69
109
  MSG_VERSION,
70
110
  headers,
71
- OpenSSL::ASN1::OctetString.new(sec_params.to_der),
111
+ OpenSSL::ASN1::OctetString.new(sec_params.to_der).with_label(:security_params),
72
112
  scoped_pdu
73
- ]).to_der
113
+ ]).with_label(:v3_message)
114
+ log(level: 2) { encoded.to_hex }
115
+
116
+ encoded = encoded.to_der
117
+ log { Hexdump.dump(encoded) }
74
118
  signature = security_parameters.sign(encoded)
75
119
  if signature
76
- auth_salt = OpenSSL::ASN1::OctetString.new(signature)
77
- encoded.sub!(AUTHNONE.to_der, auth_salt.to_der)
120
+ log { "signing V3 message..." }
121
+ auth_salt = OpenSSL::ASN1::OctetString.new(signature).with_label(:auth)
122
+ log(level: 2) { auth_salt.to_hex }
123
+ none_der = AUTHNONE.to_der
124
+ encoded[encoded.index(none_der), none_der.size] = auth_salt.to_der
125
+ log { Hexdump.dump(encoded) }
78
126
  end
79
127
  encoded
80
128
  end