netsnmp 0.0.2 → 0.1.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 +4 -4
- data/.coveralls.yml +1 -0
- data/.travis.yml +4 -4
- data/Gemfile +5 -1
- data/README.md +124 -63
- data/lib/netsnmp.rb +66 -10
- data/lib/netsnmp/client.rb +93 -75
- data/lib/netsnmp/encryption/aes.rb +84 -0
- data/lib/netsnmp/encryption/des.rb +80 -0
- data/lib/netsnmp/encryption/none.rb +17 -0
- data/lib/netsnmp/errors.rb +1 -3
- data/lib/netsnmp/message.rb +81 -0
- data/lib/netsnmp/oid.rb +18 -137
- data/lib/netsnmp/pdu.rb +106 -64
- data/lib/netsnmp/scoped_pdu.rb +23 -0
- data/lib/netsnmp/security_parameters.rb +198 -0
- data/lib/netsnmp/session.rb +84 -275
- data/lib/netsnmp/v3_session.rb +81 -0
- data/lib/netsnmp/varbind.rb +65 -156
- data/lib/netsnmp/version.rb +2 -1
- data/netsnmp.gemspec +2 -8
- data/spec/client_spec.rb +147 -99
- data/spec/handlers/celluloid_spec.rb +33 -20
- data/spec/oid_spec.rb +11 -5
- data/spec/pdu_spec.rb +22 -22
- data/spec/security_parameters_spec.rb +40 -0
- data/spec/session_spec.rb +0 -23
- data/spec/support/celluloid.rb +24 -0
- data/spec/support/request_examples.rb +36 -0
- data/spec/support/start_docker.sh +15 -1
- data/spec/v3_session_spec.rb +21 -0
- data/spec/varbind_spec.rb +2 -51
- metadata +30 -76
- data/lib/netsnmp/core.rb +0 -12
- data/lib/netsnmp/core/client.rb +0 -15
- data/lib/netsnmp/core/constants.rb +0 -153
- data/lib/netsnmp/core/inline.rb +0 -20
- data/lib/netsnmp/core/libc.rb +0 -48
- data/lib/netsnmp/core/libsnmp.rb +0 -44
- data/lib/netsnmp/core/structures.rb +0 -167
- data/lib/netsnmp/core/utilities.rb +0 -13
- data/lib/netsnmp/handlers/celluloid.rb +0 -27
- data/lib/netsnmp/handlers/em.rb +0 -56
- data/spec/core/libc_spec.rb +0 -2
- data/spec/core/libsnmp_spec.rb +0 -32
- data/spec/core/structures_spec.rb +0 -54
- data/spec/handlers/em_client_spec.rb +0 -34
data/lib/netsnmp/pdu.rb
CHANGED
@@ -1,50 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'forwardable'
|
2
3
|
module NETSNMP
|
3
4
|
# Abstracts the PDU base structure into a ruby object. It gives access to its varbinds.
|
4
5
|
#
|
5
6
|
class PDU
|
6
|
-
|
7
|
-
|
8
|
-
Error = Class.new(Error)
|
7
|
+
MAXREQUESTID=2147483647
|
9
8
|
class << self
|
9
|
+
|
10
|
+
def decode(der)
|
11
|
+
asn_tree = case der
|
12
|
+
when String
|
13
|
+
OpenSSL::ASN1.decode(der)
|
14
|
+
when OpenSSL::ASN1::ASN1Data
|
15
|
+
der
|
16
|
+
else
|
17
|
+
raise "#{der}: unexpected data"
|
18
|
+
end
|
19
|
+
|
20
|
+
*headers, request = asn_tree.value
|
21
|
+
|
22
|
+
version, community = headers.map(&:value)
|
23
|
+
|
24
|
+
type = request.tag
|
25
|
+
|
26
|
+
*request_headers, varbinds = request.value
|
27
|
+
|
28
|
+
request_id, error_status, error_index = request_headers.map(&:value).map(&:to_i)
|
29
|
+
|
30
|
+
varbs = varbinds.value.map do |varbind|
|
31
|
+
oid_asn, val_asn = varbind.value
|
32
|
+
oid = oid_asn.value
|
33
|
+
{ oid: oid, value: val_asn }
|
34
|
+
end
|
35
|
+
|
36
|
+
new(type: type, headers: [version, community],
|
37
|
+
error_status: error_status,
|
38
|
+
error_index: error_index,
|
39
|
+
request_id: request_id,
|
40
|
+
varbinds: varbs)
|
41
|
+
end
|
42
|
+
|
10
43
|
# factory method that abstracts initialization of the pdu types that the library supports.
|
11
44
|
#
|
12
45
|
# @param [Symbol] type the type of pdu structure to build
|
13
|
-
# @return [RequestPDU] a fully-formed request pdu
|
14
46
|
#
|
15
|
-
def build(type,
|
16
|
-
case type
|
17
|
-
when :get then
|
18
|
-
when :getnext then
|
19
|
-
when :getbulk then
|
20
|
-
when :set then
|
21
|
-
when :response then
|
47
|
+
def build(type, **args)
|
48
|
+
typ = case type
|
49
|
+
when :get then 0
|
50
|
+
when :getnext then 1
|
51
|
+
# when :getbulk then 5
|
52
|
+
when :set then 3
|
53
|
+
when :response then 2
|
22
54
|
else raise Error, "#{type} is not supported as type"
|
23
55
|
end
|
56
|
+
new(args.merge(type: typ))
|
24
57
|
end
|
25
58
|
end
|
26
59
|
|
27
|
-
attr_reader :
|
60
|
+
attr_reader :varbinds, :type
|
28
61
|
|
29
|
-
|
62
|
+
attr_reader :version, :community, :request_id
|
30
63
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
64
|
+
def initialize(type: , headers: ,
|
65
|
+
request_id: nil,
|
66
|
+
error_status: 0,
|
67
|
+
error_index: 0,
|
68
|
+
varbinds: [])
|
69
|
+
@version, @community = headers
|
70
|
+
@version = @version.to_i
|
71
|
+
@error_status = error_status
|
72
|
+
@error_index = error_index
|
73
|
+
@type = type
|
35
74
|
@varbinds = []
|
75
|
+
varbinds.each do |varbind|
|
76
|
+
add_varbind(varbind)
|
77
|
+
end
|
78
|
+
@request_id = request_id || SecureRandom.random_number(MAXREQUESTID)
|
79
|
+
check_error_status(@error_status)
|
36
80
|
end
|
37
81
|
|
38
82
|
|
39
|
-
|
40
|
-
|
41
|
-
# Abstracts the request PDU
|
42
|
-
# Main characteristic is that it has a write-only API, in that you can add varbinds to it.
|
43
|
-
#
|
44
|
-
class RequestPDU < PDU
|
45
|
-
def initialize(type)
|
46
|
-
pointer = Core::LibSNMP.snmp_pdu_create(type)
|
47
|
-
super(pointer)
|
83
|
+
def to_der
|
84
|
+
to_asn.to_der
|
48
85
|
end
|
49
86
|
|
50
87
|
# Adds a request varbind to the pdu
|
@@ -52,54 +89,59 @@ module NETSNMP
|
|
52
89
|
# @param [OID] oid a valid oid
|
53
90
|
# @param [Hash] options additional request varbind options
|
54
91
|
# @option options [Object] :value the value for the oid
|
55
|
-
def add_varbind(oid, **options)
|
56
|
-
@varbinds <<
|
92
|
+
def add_varbind(oid: , **options)
|
93
|
+
@varbinds << Varbind.new(oid, **options)
|
57
94
|
end
|
58
95
|
alias_method :<<, :add_varbind
|
59
|
-
end
|
60
96
|
|
61
|
-
# Abstracts the response PDU
|
62
|
-
# Main characteristic is: it reads the values on initialization (because the response structure
|
63
|
-
# is at some point free'd). It is therefore a read-only entity
|
64
|
-
#
|
65
|
-
class ResponsePDU < PDU
|
66
97
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
def initialize(pointer)
|
72
|
-
super
|
73
|
-
load_variables
|
74
|
-
end
|
98
|
+
def to_asn
|
99
|
+
request_id_asn = OpenSSL::ASN1::Integer.new( @request_id )
|
100
|
+
error_asn = OpenSSL::ASN1::Integer.new( @error_status )
|
101
|
+
error_index_asn = OpenSSL::ASN1::Integer.new( @error_index )
|
75
102
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
@varbinds.map(&:value).join(' ')
|
85
|
-
end
|
103
|
+
varbind_asns = OpenSSL::ASN1::Sequence.new( @varbinds.map(&:to_asn) )
|
104
|
+
|
105
|
+
request_asn = OpenSSL::ASN1::ASN1Data.new( [request_id_asn,
|
106
|
+
error_asn, error_index_asn,
|
107
|
+
varbind_asns], @type,
|
108
|
+
:CONTEXT_SPECIFIC )
|
109
|
+
|
110
|
+
OpenSSL::ASN1::Sequence.new( [ *encode_headers_asn, request_asn ] )
|
86
111
|
end
|
87
112
|
|
88
113
|
private
|
89
114
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
variable = @struct[:variables]
|
94
|
-
unless variable.null?
|
95
|
-
@varbinds << ResponseVarbind.new(variable)
|
96
|
-
variable = Core::Structures::VariableList.new(variable)
|
97
|
-
while( !(variable = variable[:next_variable]).null? )
|
98
|
-
variable = Core::Structures::VariableList.new(variable)
|
99
|
-
@varbinds << ResponseVarbind.new(variable.pointer)
|
100
|
-
end
|
101
|
-
end
|
115
|
+
def encode_headers_asn
|
116
|
+
[ OpenSSL::ASN1::Integer.new( @version ),
|
117
|
+
OpenSSL::ASN1::OctetString.new( @community ) ]
|
102
118
|
end
|
103
119
|
|
120
|
+
|
121
|
+
# http://www.tcpipguide.com/free/t_SNMPVersion2SNMPv2MessageFormats-5.htm#Table_219
|
122
|
+
def check_error_status(status)
|
123
|
+
return if status == 0
|
124
|
+
message = case status
|
125
|
+
when 1 then "Response-PDU too big"
|
126
|
+
when 2 then "No such name"
|
127
|
+
when 3 then "Bad value"
|
128
|
+
when 4 then "Read Only"
|
129
|
+
when 5 then "General Error"
|
130
|
+
when 6 then "Access denied"
|
131
|
+
when 7 then "Wrong type"
|
132
|
+
when 8 then "Wrong length"
|
133
|
+
when 9 then "Wrong encoding"
|
134
|
+
when 10 then "Wrong value"
|
135
|
+
when 11 then "No creation"
|
136
|
+
when 12 then "Inconsistent value"
|
137
|
+
when 13 then "Resource unavailable"
|
138
|
+
when 14 then "Commit failed"
|
139
|
+
when 15 then "Undo Failed"
|
140
|
+
when 16 then "Authorization Error"
|
141
|
+
when 17 then "Not Writable"
|
142
|
+
when 18 then "Inconsistent Name"
|
143
|
+
end
|
144
|
+
raise Error, message
|
145
|
+
end
|
104
146
|
end
|
105
147
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module NETSNMP
|
3
|
+
class ScopedPDU < PDU
|
4
|
+
|
5
|
+
|
6
|
+
attr_reader :engine_id
|
7
|
+
|
8
|
+
def initialize(type: , headers:,
|
9
|
+
request_id: nil,
|
10
|
+
error_status: 0,
|
11
|
+
error_index: 0,
|
12
|
+
varbinds: [])
|
13
|
+
@engine_id, @context = headers
|
14
|
+
super(type: type, headers: [3, nil], request_id: request_id, varbinds: varbinds)
|
15
|
+
end
|
16
|
+
|
17
|
+
def encode_headers_asn
|
18
|
+
[ OpenSSL::ASN1::OctetString.new(@engine_id || ""),
|
19
|
+
OpenSSL::ASN1::OctetString.new(@context || "") ]
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module NETSNMP
|
3
|
+
# This module encapsulates the public API for encrypting/decrypting and signing/verifying.
|
4
|
+
#
|
5
|
+
# It doesn't interact with other layers from the library, rather it is used and passed all
|
6
|
+
# the arguments (consisting mostly of primitive types).
|
7
|
+
# It also provides validation of the security options passed with a client is initialized in v3 mode.
|
8
|
+
class SecurityParameters
|
9
|
+
using StringExtensions
|
10
|
+
|
11
|
+
IPAD = "\x36" * 64
|
12
|
+
OPAD = "\x5c" * 64
|
13
|
+
|
14
|
+
attr_reader :security_level, :username
|
15
|
+
attr_accessor :engine_id
|
16
|
+
|
17
|
+
# @param [String] username the snmp v3 username
|
18
|
+
# @param [String] engine_id the device engine id (initialized to '' for report)
|
19
|
+
# @param [Symbol, integer] security_level allowed snmp v3 security level (:auth_priv, :auh_no_priv, etc)
|
20
|
+
# @param [Symbol, nil] auth_protocol a supported authentication protocol (currently supported: :md5, :sha)
|
21
|
+
# @param [Symbol, nil] priv_protocol a supported privacy protocol (currently supported: :des, :aes)
|
22
|
+
# @param [String, nil] auth_password the authentication password
|
23
|
+
# @param [String, nil] priv_password the privacy password
|
24
|
+
#
|
25
|
+
# @note if security level is set to :no_auth_no_priv, all other parameters are optional; if
|
26
|
+
# :auth_no_priv, :auth_protocol will be coerced to :md5 (if not explicitly set), and :auth_password is
|
27
|
+
# mandatory; if :auth_priv, the sentence before applies, and :priv_protocol will be coerced to :des (if
|
28
|
+
# not explicitly set), and :priv_password becomes mandatory.
|
29
|
+
#
|
30
|
+
def initialize(
|
31
|
+
username: ,
|
32
|
+
engine_id: "",
|
33
|
+
security_level: nil,
|
34
|
+
auth_protocol: nil,
|
35
|
+
auth_password: nil,
|
36
|
+
priv_protocol: nil,
|
37
|
+
priv_password: nil)
|
38
|
+
@security_level = security_level
|
39
|
+
@username = username
|
40
|
+
@engine_id = engine_id
|
41
|
+
@auth_protocol = auth_protocol.to_sym unless auth_protocol.nil?
|
42
|
+
@priv_protocol = priv_protocol.to_sym unless priv_protocol.nil?
|
43
|
+
@auth_password = auth_password
|
44
|
+
@priv_password = priv_password
|
45
|
+
check_parameters
|
46
|
+
@auth_pass_key = passkey(@auth_password) unless @auth_password.nil?
|
47
|
+
@priv_pass_key = passkey(@priv_password) unless @priv_password.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# @param [#to_asn, #to_der] pdu the pdu to encode (must quack like a asn1 type)
|
52
|
+
# @param [String] salt the salt to use
|
53
|
+
# @param [Integer] engine_time the reported engine time
|
54
|
+
# @param [Integer] engine_boots the reported boots time
|
55
|
+
#
|
56
|
+
# @return [Array] a pair, where the first argument in the asn structure with the encoded pdu,
|
57
|
+
# and the second is the calculated salt (if it has been encrypted)
|
58
|
+
def encode(pdu, salt: , engine_time: , engine_boots: )
|
59
|
+
if encryption
|
60
|
+
encrypted_pdu, salt = encryption.encrypt(pdu.to_der, engine_boots: engine_boots,
|
61
|
+
engine_time: engine_time)
|
62
|
+
[OpenSSL::ASN1::OctetString.new(encrypted_pdu), OpenSSL::ASN1::OctetString.new(salt) ]
|
63
|
+
else
|
64
|
+
[ pdu.to_asn, salt ]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param [String] der the encoded der to be decoded
|
69
|
+
# @param [String] salt the salt from the incoming der
|
70
|
+
# @param [Integer] engine_time the reported engine time
|
71
|
+
# @param [Integer] engine_boots the reported engine boots
|
72
|
+
def decode(der, salt: , engine_time: , engine_boots: )
|
73
|
+
asn = OpenSSL::ASN1.decode(der)
|
74
|
+
if encryption
|
75
|
+
encrypted_pdu = asn.value
|
76
|
+
pdu_der = encryption.decrypt(encrypted_pdu, salt: salt, engine_time: engine_time, engine_boots: engine_boots)
|
77
|
+
OpenSSL::ASN1.decode(pdu_der)
|
78
|
+
else
|
79
|
+
asn
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param [String] message the already encoded snmp v3 message
|
84
|
+
# @return [String] the digest signature of the message payload
|
85
|
+
#
|
86
|
+
# @note this method is used in the process of authenticating a message
|
87
|
+
def sign(message)
|
88
|
+
# don't sign unless you have to
|
89
|
+
return nil if not @auth_protocol
|
90
|
+
|
91
|
+
key = auth_key.dup
|
92
|
+
|
93
|
+
key << "\x00" * (@auth_protocol == :md5 ? 48 : 44)
|
94
|
+
k1 = key.xor(IPAD)
|
95
|
+
k2 = key.xor(OPAD)
|
96
|
+
|
97
|
+
digest.reset
|
98
|
+
digest << ( k1 + message )
|
99
|
+
d1 = digest.digest
|
100
|
+
|
101
|
+
digest.reset
|
102
|
+
digest << ( k2 + d1 )
|
103
|
+
digest.digest[0,12]
|
104
|
+
end
|
105
|
+
|
106
|
+
# @param [String] stream the encoded incoming payload
|
107
|
+
# @param [String] salt the incoming payload''s salt
|
108
|
+
#
|
109
|
+
# @raise [NETSMP::Error] if the message's integration has been violated
|
110
|
+
def verify(stream, salt)
|
111
|
+
return if @security_level < 1
|
112
|
+
verisalt = sign(stream)
|
113
|
+
raise Error, "invalid message authentication salt" unless verisalt == salt
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def auth_key
|
119
|
+
@auth_key ||= localize_key(@auth_pass_key)
|
120
|
+
end
|
121
|
+
|
122
|
+
def priv_key
|
123
|
+
@priv_key ||= localize_key(@priv_pass_key)
|
124
|
+
end
|
125
|
+
|
126
|
+
def check_parameters
|
127
|
+
@security_level = case @security_level
|
128
|
+
when Integer then @security_level
|
129
|
+
when /no_?auth/ then 0
|
130
|
+
when /auth_?no_?priv/ then 1
|
131
|
+
when /auth_?priv/, nil then 3
|
132
|
+
else
|
133
|
+
raise Error, "security level not supported: #{@security_level}"
|
134
|
+
end
|
135
|
+
|
136
|
+
if @security_level > 0
|
137
|
+
@auth_protocol ||= :md5 # this is the default
|
138
|
+
raise "security level requires an auth password" if @auth_password.nil?
|
139
|
+
raise "auth password must have between 8 to 32 characters" if not (8..32).include?(@auth_password.length)
|
140
|
+
end
|
141
|
+
if @security_level > 1
|
142
|
+
@priv_protocol ||= :des
|
143
|
+
raise "security level requires a priv password" if @priv_password.nil?
|
144
|
+
raise "priv password must have between 8 to 32 characters" if not (8..32).include?(@priv_password.length)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def localize_key(key)
|
149
|
+
|
150
|
+
digest.reset
|
151
|
+
digest << key
|
152
|
+
digest << @engine_id
|
153
|
+
digest << key
|
154
|
+
|
155
|
+
digest.digest
|
156
|
+
end
|
157
|
+
|
158
|
+
def passkey(password)
|
159
|
+
|
160
|
+
digest.reset
|
161
|
+
password_index = 0
|
162
|
+
|
163
|
+
buffer = String.new
|
164
|
+
password_length = password.length
|
165
|
+
while password_index < 1048576
|
166
|
+
initial = password_index % password_length
|
167
|
+
rotated = password[initial..-1] + password[0,initial]
|
168
|
+
buffer = rotated * (64 / rotated.length) + rotated[0, 64 % rotated.length]
|
169
|
+
password_index += 64
|
170
|
+
digest << buffer
|
171
|
+
buffer.clear
|
172
|
+
end
|
173
|
+
|
174
|
+
dig = digest.digest
|
175
|
+
dig = dig[0,16] if @auth_protocol == :md5
|
176
|
+
dig
|
177
|
+
end
|
178
|
+
|
179
|
+
def digest
|
180
|
+
@digest ||= case @auth_protocol
|
181
|
+
when :md5 then OpenSSL::Digest::MD5.new
|
182
|
+
when :sha then OpenSSL::Digest::SHA1.new
|
183
|
+
else
|
184
|
+
raise Error, "unsupported auth protocol: #{@auth_protocol}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def encryption
|
189
|
+
@encryption ||= case @priv_protocol
|
190
|
+
when :des
|
191
|
+
Encryption::DES.new(priv_key)
|
192
|
+
when :aes
|
193
|
+
Encryption::AES.new(priv_key)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
end
|
data/lib/netsnmp/session.rb
CHANGED
@@ -1,317 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module NETSNMP
|
2
|
-
#
|
3
|
+
# Let's just remind that there is no session in snmp, this is just an abstraction.
|
3
4
|
#
|
4
|
-
# For example, a session must be initialized (memory allocated) and opened
|
5
|
-
# (authentication, encryption, signature)
|
6
|
-
#
|
7
|
-
# The session uses the signature to send and receive PDUs. They are built somewhere else.
|
8
|
-
#
|
9
|
-
# After the session is established, a socket handle is read from the structure. This will
|
10
|
-
# be later used for non-blocking behaviour. It's important to notice, there is no
|
11
|
-
# usage of the C net-snmp sync API, we always do async send/response, even if the
|
12
|
-
# ruby API "feels" blocking. This was done so that the GIL can be released between
|
13
|
-
# sends and receives, and the load can be shared through different threads possibly.
|
14
|
-
# As we use the session abstraction, this means we ONLY use the thread-safe API.
|
15
|
-
#
|
16
5
|
class Session
|
6
|
+
TIMEOUT = 2
|
17
7
|
|
18
|
-
attr_reader :host, :signature
|
19
|
-
|
20
|
-
# @param [String] host the host IP/hostname
|
21
8
|
# @param [Hash] opts the options set
|
22
|
-
|
23
|
-
|
24
|
-
@
|
25
|
-
|
26
|
-
# give you better trackability of errors. Give the opportunity to the users to
|
27
|
-
# pass it, by setting the hostname explicitly. If not, fallback to the host.
|
28
|
-
@hostname = opts.fetch(:hostname, @host)
|
29
|
-
@options = opts
|
30
|
-
@logged_at = nil
|
31
|
-
@request = nil
|
32
|
-
# For now, let's eager load the signature
|
33
|
-
@signature = build_signature(@options)
|
34
|
-
if @signature.null?
|
35
|
-
raise ConnectionFailed, "could not build signature for #@hostname"
|
36
|
-
end
|
37
|
-
@requests ||= {}
|
38
|
-
end
|
39
|
-
|
40
|
-
# TODO: do we need this?
|
41
|
-
def reachable?
|
42
|
-
!!transport
|
9
|
+
def initialize(version: 1, community: "public", **options)
|
10
|
+
@version = 1
|
11
|
+
@community = community
|
12
|
+
validate(options)
|
43
13
|
end
|
44
14
|
|
45
15
|
# Closes the session
|
46
16
|
def close
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
51
|
-
if Core::LibSNMP.snmp_sess_close(@signature) == 0
|
52
|
-
raise Error, "#@hostname: Couldn't clean up session properly"
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
# sends a request PDU and waits for the response
|
57
|
-
#
|
58
|
-
# @param [RequestPDU] pdu a request pdu
|
59
|
-
# @param [Hash] opts additional options
|
60
|
-
# @option opts [true, false] :async if true, it doesn't wait for response (defaults to false)
|
61
|
-
def send(pdu, **opts)
|
62
|
-
write(pdu)
|
63
|
-
read
|
64
|
-
end
|
65
|
-
|
66
|
-
private
|
67
|
-
|
68
|
-
LoggedInTimeout = Class.new(Timeout::Error)
|
69
|
-
def try_login
|
70
|
-
return yield unless @logged_at.nil?
|
71
|
-
|
72
|
-
begin
|
73
|
-
# problem for snmp is, there is no login process.
|
74
|
-
# signature is sent on first packet. As we are not using the synch interface, this will not
|
75
|
-
# be handled by the core library. Which sucks. But even core library would just retry and timeout
|
76
|
-
# at some point.
|
77
|
-
# we do something similar: before any PDU succeeds, after first PDU is async-sent, we wait for reads, but we can't block. If
|
78
|
-
# the socket hasn't anything to read, we can assume it was the wrong USERNAME (?).
|
79
|
-
# TODO: research if this is true.
|
80
|
-
Timeout.timeout(@timeout, LoggedInTimeout) do
|
81
|
-
yield
|
82
|
-
end
|
83
|
-
rescue LoggedInTimeout
|
84
|
-
raise ConnectionFailed, "failed to login to #@hostname"
|
85
|
-
end
|
17
|
+
# if the transport came as an argument,
|
18
|
+
# then let the outer realm care for its lifecycle
|
19
|
+
@transport.close unless @proxy
|
86
20
|
end
|
87
21
|
|
88
|
-
|
89
|
-
|
22
|
+
# @param [Symbol] type the type of PDU (:get, :set, :getnext)
|
23
|
+
# @param [Array<Hashes>] vars collection of options to generate varbinds (see {NETSMP::Varbind.new} for all the possible options)
|
24
|
+
#
|
25
|
+
# @return [NETSNMP::PDU] a pdu
|
26
|
+
#
|
27
|
+
def build_pdu(type, *vars)
|
28
|
+
PDU.build(type, headers: [ @version, @community ], varbinds: vars)
|
90
29
|
end
|
91
30
|
|
92
|
-
|
93
|
-
|
94
|
-
|
31
|
+
# send a pdu, receives a pdu
|
32
|
+
#
|
33
|
+
# @param [NETSNMP::PDU, #to_der] an encodable request pdu
|
34
|
+
#
|
35
|
+
# @return [NETSNMP::PDU] the response pdu
|
36
|
+
#
|
37
|
+
def send(pdu)
|
38
|
+
encoded_request = encode(pdu)
|
39
|
+
encoded_response = @transport.send(encoded_request)
|
40
|
+
decode(encoded_response)
|
95
41
|
end
|
96
42
|
|
97
|
-
|
98
|
-
if ( @reqid = Core::LibSNMP.snmp_sess_async_send(@signature, pdu.pointer, session_callback, nil) ) == 0
|
99
|
-
# it's interesting, pdu's are only fred if the async send is successful... netsnmp 1 - me 0
|
100
|
-
Core::LibSNMP.snmp_free_pdu(pdu.pointer)
|
101
|
-
# if it's the first time we're passing here and send fails, we can (?) assume that
|
102
|
-
# AUTH_PASSWORD is wrong
|
103
|
-
if @logged_at.nil?
|
104
|
-
raise ConnectionFailed, "failed to login to #@hostname"
|
105
|
-
else
|
106
|
-
raise SendError, "#@hostname: Failed to send pdu"
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def read
|
112
|
-
receive # trigger callback ahead of time and wait for it
|
113
|
-
# Sounds a bit unreasonable, but only after we arrived here we know for sure that the credentials are proper.
|
114
|
-
# So we can set this variable, so further errors can be safely ignored.
|
115
|
-
@logged_at ||= Time.now
|
116
|
-
handle_response
|
117
|
-
end
|
118
|
-
|
119
|
-
def handle_response
|
120
|
-
operation, response_pdu = @requests.delete(@reqid)
|
121
|
-
case operation
|
122
|
-
when :success
|
123
|
-
response_pdu
|
124
|
-
when :send_failed
|
125
|
-
raise ReceiveError, "#@hostname: Failed to receive pdu"
|
126
|
-
when :timeout
|
127
|
-
raise Timeout::Error, "#@hostname: timed out while waiting for pdu response"
|
128
|
-
else
|
129
|
-
raise Error, "#@hostname: unrecognized operation for request #{@reqid}: #{operation} for #{response_pdu}"
|
130
|
-
end
|
131
|
-
end
|
43
|
+
private
|
132
44
|
|
133
|
-
def
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
45
|
+
def validate(**options)
|
46
|
+
proxy = options[:proxy]
|
47
|
+
if proxy
|
48
|
+
@proxy = true
|
49
|
+
@transport = proxy
|
50
|
+
else
|
51
|
+
host, port = options.values_at(:host, :port)
|
52
|
+
raise "you must provide an hostname/ip under :host" unless host
|
53
|
+
port ||= 161 # default snmp port
|
54
|
+
@transport = Transport.new(host, port.to_i, timeout: options.fetch(:timeout, TIMEOUT))
|
143
55
|
end
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
# PRIV_PASSWORD is wrong
|
150
|
-
if @logged_at.nil?
|
151
|
-
raise ConnectionFailed, "failed to login to #@hostname"
|
56
|
+
@version = case @version
|
57
|
+
when Integer then @version # assume the use know what he's doing
|
58
|
+
when /v?1/ then 0
|
59
|
+
when /v?2c?/ then 1
|
60
|
+
when /v?3/ then 3
|
152
61
|
else
|
153
|
-
raise
|
154
|
-
end
|
62
|
+
raise "unsupported snmp version (#{@version})"
|
155
63
|
end
|
156
64
|
end
|
157
65
|
|
158
|
-
def timeout
|
159
|
-
Core::LibSNMP.snmp_sess_timeout(@signature)
|
160
|
-
end
|
161
|
-
|
162
|
-
def wait_writable
|
163
|
-
IO.select([],[transport])
|
164
|
-
end
|
165
66
|
|
166
|
-
def
|
167
|
-
|
67
|
+
def encode(pdu)
|
68
|
+
pdu.to_der
|
168
69
|
end
|
169
70
|
|
170
|
-
def
|
171
|
-
|
172
|
-
fdset.clear
|
173
|
-
num_fds = FFI::MemoryPointer.new(:int)
|
174
|
-
tv_sec = 0
|
175
|
-
tv_usec = 0
|
176
|
-
tval = Core::C::Timeval.new
|
177
|
-
tval[:tv_sec] = tv_sec
|
178
|
-
tval[:tv_usec] = tv_usec
|
179
|
-
block = FFI::MemoryPointer.new(:int)
|
180
|
-
block.write_int(0)
|
181
|
-
Core::LibSNMP.snmp_sess_select_info(@signature, num_fds, fdset.pointer, tval.pointer, block )
|
182
|
-
fdset
|
71
|
+
def decode(stream)
|
72
|
+
PDU.decode(stream)
|
183
73
|
end
|
184
74
|
|
75
|
+
class Transport
|
76
|
+
MAXPDUSIZE = 0xffff + 1
|
185
77
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
# @option options [Symbol, nil] :auth_protocol the authorization protocol (ex: :md5, :sha1)
|
191
|
-
# @option options [Symbol, nil] :priv_protocol the privacy protocol (ex: :aes, :des)
|
192
|
-
# @option options [String, nil] :context the authoritative context
|
193
|
-
# @option options [String] :version the snmp protocol version (defaults to 3, if not 3, you actually don't need the rest)
|
194
|
-
# @option options [String] :username the username to login with
|
195
|
-
# @option options [String] :auth_password the authorization password
|
196
|
-
# @option options [String] :priv_password the privacy password
|
197
|
-
def session_authorization(session, options)
|
198
|
-
# we support version 3 by default
|
199
|
-
session[:version] = case options[:version]
|
200
|
-
when /v?1/ then Core::Constants::SNMP_VERSION_1
|
201
|
-
when /v?2c?/ then Core::Constants::SNMP_VERSION_2c
|
202
|
-
when /v?3/, nil then Core::Constants::SNMP_VERSION_3
|
78
|
+
def initialize(host, port, timeout: )
|
79
|
+
@socket = UDPSocket.new
|
80
|
+
@socket.connect( host, port )
|
81
|
+
@timeout = timeout
|
203
82
|
end
|
204
|
-
return unless session[:version] == Core::Constants::SNMP_VERSION_3
|
205
83
|
|
206
|
-
|
207
|
-
|
208
|
-
session[:securityAuthKeyLen] = Core::Constants::USM_AUTH_KU_LEN
|
209
|
-
session[:securityPrivProtoLen] = 10
|
210
|
-
session[:securityPrivKeyLen] = Core::Constants::USM_PRIV_KU_LEN
|
211
|
-
|
212
|
-
# Security Authorization
|
213
|
-
session[:securityLevel] = case options[:security_level]
|
214
|
-
when /noauth/ then Core::Constants::SNMP_SEC_LEVEL_NOAUTH
|
215
|
-
when /auth_?no_?priv/ then Core::Constants::SNMP_SEC_LEVEL_AUTHNOPRIV
|
216
|
-
when /auth_?priv/ then Core::Constants::SNMP_SEC_LEVEL_AUTHPRIV
|
217
|
-
when Integer
|
218
|
-
options[:security_level]
|
219
|
-
else Core::Constants::SNMP_SEC_LEVEL_AUTHPRIV
|
220
|
-
end
|
221
|
-
|
222
|
-
auth_protocol_oid = case options[:auth_protocol]
|
223
|
-
when :md5 then MD5OID.new
|
224
|
-
when :sha1 then SHA1OID.new
|
225
|
-
when nil then NoAuthOID.new
|
226
|
-
else raise Error, "#@hostname: #{options[:auth_protocol]} is an unsupported authorization protocol"
|
84
|
+
def close
|
85
|
+
@socket.close
|
227
86
|
end
|
228
87
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
when :des then DESOID.new
|
233
|
-
when nil then NoPrivOID.new
|
234
|
-
else raise Error, "#@hostname: #{options[:priv_protocol]} is an unsupported privacy protocol"
|
88
|
+
def send(payload)
|
89
|
+
write(payload)
|
90
|
+
recv
|
235
91
|
end
|
236
|
-
|
237
|
-
user, auth_pass, priv_pass = options.values_at(:username, :auth_password, :priv_password)
|
238
|
-
auth_protocol_oid.generate_key(session, user, auth_pass)
|
239
|
-
priv_protocol_oid.generate_key(session, user, priv_pass )
|
240
92
|
|
241
|
-
|
242
|
-
|
243
|
-
|
93
|
+
def write(payload)
|
94
|
+
perform_io do
|
95
|
+
@socket.send(payload, 0)
|
96
|
+
end
|
244
97
|
end
|
245
98
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
# @option options [String] :community the snmp community string (defaults to public)
|
252
|
-
# @option options [Integer] :timeout number of sec until first timeout
|
253
|
-
# @option options [Integer] :retries number of retries before timeout
|
254
|
-
# @return [FFI::Pointer] a pointer to the validated session signature, which will therefore be used in all _sess_ methods from libnetsnmp
|
255
|
-
def build_signature(options)
|
256
|
-
# allocate new session
|
257
|
-
session = Core::Structures::Session.new(nil)
|
258
|
-
Core::LibSNMP.snmp_sess_init(session.pointer)
|
259
|
-
|
260
|
-
# initialize session
|
261
|
-
if options[:community]
|
262
|
-
community = options[:community]
|
263
|
-
session[:community] = FFI::MemoryPointer.from_string(community)
|
264
|
-
session[:community_len] = community.length
|
99
|
+
def recv(bytesize=MAXPDUSIZE)
|
100
|
+
perform_io do
|
101
|
+
datagram, _ = @socket.recvfrom_nonblock(bytesize)
|
102
|
+
datagram
|
103
|
+
end
|
265
104
|
end
|
266
|
-
|
267
|
-
peername = host
|
268
|
-
unless peername[':']
|
269
|
-
port = options[:port] || '161'.freeze
|
270
|
-
peername = "#{peername}:#{port}"
|
271
|
-
end
|
272
|
-
|
273
|
-
session[:peername] = FFI::MemoryPointer.from_string(peername)
|
274
|
-
|
275
|
-
@timeout = options[:timeout] || 10
|
276
|
-
session[:timeout] = @timeout * 1000000
|
277
|
-
session[:retries] = options[:retries] || 5
|
278
|
-
session_authorization(session, options)
|
279
|
-
Core::LibSNMP.snmp_sess_open(session.pointer)
|
280
|
-
end
|
281
105
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
op = case operation
|
294
|
-
when Core::Constants::NETSNMP_CALLBACK_OP_RECEIVED_MESSAGE then :success
|
295
|
-
when Core::Constants::NETSNMP_CALLBACK_OP_TIMED_OUT then :timeout
|
296
|
-
when Core::Constants::NETSNMP_CALLBACK_OP_SEND_FAILED then :send_failed
|
297
|
-
when Core::Constants::NETSNMP_CALLBACK_OP_CONNECT then :connect
|
298
|
-
when Core::Constants::NETSNMP_CALLBACK_OP_DISCONNECT then :disconnect
|
299
|
-
else :unrecognized_operation
|
106
|
+
private
|
107
|
+
|
108
|
+
def perform_io
|
109
|
+
loop do
|
110
|
+
begin
|
111
|
+
return yield
|
112
|
+
rescue IO::WaitReadable
|
113
|
+
wait(:wait_readable)
|
114
|
+
rescue IO::WaitWritable
|
115
|
+
wait(:wait_writable)
|
116
|
+
end
|
300
117
|
end
|
118
|
+
end
|
301
119
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
@requests[@reqid] = [op, response_pdu]
|
307
|
-
if reqid == @reqid
|
308
|
-
# probably pass the result as a yield from a fiber
|
309
|
-
op.eql?(:unrecognized_operation) ? 0 : 1
|
310
|
-
else
|
311
|
-
# this is happening when user is unknown(????)
|
312
|
-
#puts "wow, unexpected #{op}.... #{reqid} different than #{@reqid}"
|
313
|
-
0
|
314
|
-
end
|
120
|
+
def wait(mode)
|
121
|
+
unless @socket.__send__(mode, @timeout)
|
122
|
+
raise Timeout::Error, "Timeout after #{@timeout} seconds"
|
123
|
+
end
|
315
124
|
end
|
316
125
|
|
317
126
|
end
|