netsnmp 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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