netsnmp 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|