fluent-plugin-secure-forward-addproxy 0.3.3dev2

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.
@@ -0,0 +1,280 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'fluent/mixin/config_placeholders'
4
+
5
+ module Fluent
6
+ class SecureForwardOutput < ObjectBufferedOutput
7
+ end
8
+ end
9
+
10
+ require_relative 'output_node'
11
+
12
+ module Fluent
13
+ class SecureForwardOutput < ObjectBufferedOutput
14
+ DEFAULT_SECURE_CONNECT_PORT = 24284
15
+
16
+ Fluent::Plugin.register_output('secure_forward', self)
17
+
18
+ config_param :secure, :bool
19
+
20
+ config_param :self_hostname, :string
21
+ include Fluent::Mixin::ConfigPlaceholders
22
+
23
+ config_param :shared_key, :string
24
+
25
+ config_param :keepalive, :time, default: nil # nil/0 means disable keepalive expiration
26
+
27
+ config_param :send_timeout, :time, default: 60
28
+ # config_param :hard_timeout, :time, :default => 60
29
+ # config_param :expire_dns_cache, :time, :default => 0 # 0 means disable cache
30
+
31
+ config_param :ca_cert_path, :string, default: nil
32
+
33
+ config_param :enable_strict_verification, :bool, default: nil # FQDN check with hostlabel
34
+ config_param :ssl_version, :string, default: 'TLSv1_2'
35
+ config_param :ssl_ciphers, :string, default: nil
36
+
37
+ config_param :read_length, :size, default: 512 # 512bytes
38
+ config_param :read_interval_msec, :integer, default: 50 # 50ms
39
+ config_param :socket_interval_msec, :integer, default: 200 # 200ms
40
+
41
+ config_param :reconnect_interval, :time, default: 5
42
+ config_param :established_timeout, :time, default: 10
43
+
44
+ config_param :proxy_uri, :string, default: nil
45
+
46
+ attr_reader :read_interval, :socket_interval
47
+
48
+ config_section :server, param_name: :servers do
49
+ config_param :host, :string
50
+ config_param :hostlabel, :string, default: nil
51
+ config_param :port, :integer, default: DEFAULT_SECURE_CONNECT_PORT
52
+ config_param :shared_key, :string, default: nil
53
+ config_param :username, :string, default: ''
54
+ config_param :password, :string, default: ''
55
+ config_param :standby, :bool, default: false
56
+ config_param :proxy_uri, :string, default: nil
57
+ end
58
+ attr_reader :nodes
59
+
60
+ attr_reader :hostname_resolver
61
+
62
+ def initialize
63
+ super
64
+ require 'socket'
65
+ require 'openssl'
66
+ require 'digest'
67
+ require 'resolve/hostname'
68
+ require 'securerandom'
69
+ end
70
+
71
+ # Define `log` method for v0.10.42 or earlier
72
+ unless method_defined?(:log)
73
+ define_method("log") { $log }
74
+ end
75
+
76
+ def configure(conf)
77
+ super
78
+
79
+ if @secure
80
+ if @ca_cert_path
81
+ raise Fluent::ConfigError, "CA cert file not found nor readable at '#{@ca_cert_path}'" unless File.readable?(@ca_cert_path)
82
+ begin
83
+ OpenSSL::X509::Certificate.new File.read(@ca_cert_path)
84
+ rescue OpenSSL::X509::CertificateError => e
85
+ raise Fluent::ConfigError, "failed to load CA cert file"
86
+ end
87
+ else
88
+ raise Fluent::ConfigError, "FQDN verification required for certificates issued from public CA" unless @enable_strict_verification
89
+ log.info "secure connection with valid certificates issued from public CA"
90
+ end
91
+ else
92
+ log.warn "'insecure' mode has vulnerability for man-in-the-middle attacks."
93
+ end
94
+
95
+ @read_interval = @read_interval_msec / 1000.0
96
+ @socket_interval = @socket_interval_msec / 1000.0
97
+
98
+ @nodes = []
99
+ @servers.each do |server|
100
+ node = Node.new(self, server)
101
+ node.first_session = true
102
+ @nodes.push node
103
+ end
104
+
105
+ if @num_threads > @nodes.select{|n| not n.standby}.size
106
+ log.warn "Too many num_threads for secure-forward: threads should be smaller or equal to non standby servers"
107
+ end
108
+
109
+ @next_node = 0
110
+ @mutex = Mutex.new
111
+
112
+ @hostname_resolver = Resolve::Hostname.new(system_resolver: true)
113
+
114
+ true
115
+ end
116
+
117
+ def select_node(permit_standby=false)
118
+ tries = 0
119
+ nodes = @nodes.size
120
+ @mutex.synchronize {
121
+ n = nil
122
+ while tries <= nodes
123
+ n = @nodes[@next_node]
124
+ @next_node += 1
125
+ @next_node = 0 if @next_node >= nodes
126
+
127
+ if n && n.established? && (! n.tained?) && (! n.detached?) && (!n.standby || permit_standby)
128
+ n.tain!
129
+ return n
130
+ end
131
+
132
+ tries += 1
133
+ end
134
+ nil
135
+ }
136
+ end
137
+
138
+ def start
139
+ super
140
+
141
+ log.debug "starting secure-forward"
142
+ OpenSSL::Random.seed(SecureRandom.random_bytes(16))
143
+ log.debug "start to connect target nodes"
144
+ @nodes.each do |node|
145
+ log.debug "connecting node", host: node.host, port: node.port
146
+ node.start
147
+ end
148
+ @nodewatcher = Thread.new(&method(:node_watcher))
149
+ @nodewatcher.abort_on_exception = true
150
+ end
151
+
152
+ def node_watcher
153
+ reconnectings = Array.new(@nodes.size)
154
+ nodes_size = @nodes.size
155
+
156
+ loop do
157
+ sleep @reconnect_interval
158
+
159
+ log.trace "in node health watcher"
160
+
161
+ (0...nodes_size).each do |i|
162
+ log.trace "node health watcher for #{@nodes[i].host}"
163
+
164
+ next if @nodes[i].established? && ! @nodes[i].expired? && ! @nodes[i].detached?
165
+
166
+ next if reconnectings[i]
167
+
168
+ reason = :expired
169
+
170
+ unless @nodes[i].established?
171
+ log.warn "dead connection found: #{@nodes[i].host}, reconnecting..."
172
+ reason = :dead
173
+ end
174
+
175
+ node = @nodes[i]
176
+ log.debug "reconnecting to node", host: node.host, port: node.port, expire: node.expire, expired: node.expired?, detached: node.detached?
177
+
178
+ renewed = node.dup
179
+ renewed.start
180
+
181
+ Thread.pass # to connection thread
182
+ reconnectings[i] = { conn: renewed, at: Time.now, reason: reason }
183
+ end
184
+
185
+ (0...nodes_size).each do |i|
186
+ next unless reconnectings[i]
187
+
188
+ log.trace "checking reconnecting node #{reconnectings[i][:conn].host}"
189
+
190
+ if reconnectings[i][:conn].established?
191
+ log.debug "connection established for reconnecting node"
192
+
193
+ oldconn = @nodes[i]
194
+ @nodes[i] = reconnectings[i][:conn]
195
+
196
+ if reconnectings[i][:reason] == :dead
197
+ log.warn "recovered connection to dead node: #{nodes[i].host}"
198
+ end
199
+
200
+ log.trace "old connection shutting down"
201
+ oldconn.detach! if oldconn # connection object doesn't raise any exceptions
202
+ log.trace "old connection shutted down"
203
+
204
+ reconnectings[i] = nil
205
+ next
206
+ end
207
+
208
+ # not connected yet
209
+
210
+ next if reconnectings[i][:at] + @established_timeout > Time.now
211
+
212
+ # not connected yet, and timeout
213
+ timeout_conn = reconnectings[i][:conn]
214
+ log.debug "SSL connection is not established until timemout", host: timeout_conn.host, port: timeout_conn.port, timeout: @established_timeout
215
+ reconnectings[i] = nil
216
+ timeout_conn.detach! if timeout_conn # connection object doesn't raise any exceptions
217
+ end
218
+ end
219
+ end
220
+
221
+ def shutdown
222
+ super
223
+
224
+ @nodewatcher.kill
225
+ @nodewatcher.join
226
+
227
+ @nodes.each do |node|
228
+ node.detach!
229
+ node.join
230
+ end
231
+ end
232
+
233
+ def write_objects(tag, es)
234
+ node = select_node || select_node(true)
235
+ unless node
236
+ raise "no one nodes with valid ssl session"
237
+ end
238
+ log.trace "selected node", host: node.host, port: node.port, standby: node.standby
239
+
240
+ begin
241
+ send_data(node, tag, es)
242
+ node.release!
243
+ rescue Errno::EPIPE, IOError, OpenSSL::SSL::SSLError => e
244
+ log.warn "Failed to send messages to #{node.host}, parging.", error_class: e.class, error: e
245
+ node.release!
246
+ node.detach!
247
+
248
+ raise # to retry #write_objects
249
+ end
250
+ end
251
+
252
+ # MessagePack FixArray length = 2
253
+ FORWARD_HEADER = [0x92].pack('C')
254
+
255
+ # to forward messages
256
+ def send_data(node, tag, es)
257
+ ssl = node.sslsession
258
+ # beginArray(2)
259
+ ssl.write FORWARD_HEADER
260
+
261
+ # writeRaw(tag)
262
+ ssl.write tag.to_msgpack
263
+
264
+ # beginRaw(size)
265
+ sz = es.size
266
+ # # FixRaw
267
+ # ssl.write [0xa0 | sz].pack('C')
268
+ #elsif sz < 65536
269
+ # # raw 16
270
+ # ssl.write [0xda, sz].pack('Cn')
271
+ #else
272
+ # raw 32
273
+ ssl.write [0xdb, sz].pack('CN')
274
+ #end
275
+
276
+ # writeRawBody(packed_es)
277
+ es.write_to(ssl)
278
+ end
279
+ end
280
+ end
@@ -0,0 +1,348 @@
1
+ # require 'msgpack'
2
+ # require 'socket'
3
+ # require 'openssl'
4
+ # require 'digest'
5
+ # require 'resolve/hostname'
6
+
7
+ require_relative 'openssl_util'
8
+
9
+ class Fluent::SecureForwardOutput::Node
10
+ attr_accessor :host, :port, :hostlabel, :shared_key, :username, :password, :standby
11
+
12
+ attr_accessor :authentication, :keepalive
13
+ attr_accessor :socket, :sslsession, :unpacker, :shared_key_salt, :state
14
+
15
+ attr_accessor :first_session, :detach
16
+
17
+ attr_reader :expire
18
+
19
+ def initialize(sender, conf)
20
+ @sender = sender
21
+ @shared_key = conf.shared_key || sender.shared_key
22
+
23
+ @host = conf.host
24
+ @port = conf.port
25
+ @hostlabel = conf.hostlabel || conf.host
26
+ @username = conf.username
27
+ @password = conf.password
28
+ @standby = conf.standby
29
+
30
+ @proxy_uri = conf.proxy_uri
31
+
32
+ @keepalive = sender.keepalive
33
+
34
+ @authentication = nil
35
+
36
+ @writing = false
37
+
38
+ @expire = nil
39
+ @first_session = false
40
+ @detach = false
41
+
42
+ @socket = nil
43
+ @sslsession = nil
44
+ @unpacker = MessagePack::Unpacker.new
45
+
46
+ @shared_key_salt = generate_salt
47
+ @state = :helo
48
+ @thread = nil
49
+ end
50
+
51
+ def log
52
+ @sender.log
53
+ end
54
+
55
+ def dup
56
+ renewed = self.class.new(
57
+ @sender,
58
+ Fluent::Config::Section.new({host: @host, port: @port, hostlabel: @hostlabel, username: @username, password: @password, shared_key: @shared_key, standby: @standby, proxy_uri: @proxy_uri})
59
+ )
60
+ renewed
61
+ end
62
+
63
+ def start
64
+ @thread = Thread.new(&method(:connect))
65
+ end
66
+
67
+ def detach!
68
+ @detach = true
69
+ end
70
+
71
+ def detached?
72
+ @detach
73
+ end
74
+
75
+ def tain!
76
+ raise RuntimeError, "BUG: taining detached node" if @detach
77
+ @writing = true
78
+ end
79
+
80
+ def tained?
81
+ @writing
82
+ end
83
+
84
+ def release!
85
+ @writing = false
86
+ end
87
+
88
+ def shutdown
89
+ log.debug "shutting down node #{@host}"
90
+ @state = :closed
91
+
92
+ if @thread == Thread.current
93
+ @sslsession.close if @sslsession
94
+ @socket.close if @socket
95
+ @thread.kill
96
+ else
97
+ if @thread
98
+ @thread.kill
99
+ @thread.join
100
+ end
101
+ @sslsession.close if @sslsession
102
+ @socket.close if @socket
103
+ end
104
+ rescue => e
105
+ log.debug "error on node shutdown #{e.class}:#{e.message}"
106
+ end
107
+
108
+ def join
109
+ @thread && @thread.join
110
+ end
111
+
112
+ def established?
113
+ @state == :established
114
+ end
115
+
116
+ def expired?
117
+ if @keepalive.nil? || @keepalive == 0
118
+ false
119
+ else
120
+ @expire && @expire < Time.now
121
+ end
122
+ end
123
+
124
+ def generate_salt
125
+ OpenSSL::Random.random_bytes(16)
126
+ end
127
+
128
+ def check_helo(message)
129
+ log.debug "checking helo"
130
+ # ['HELO', options(hash)]
131
+ unless message.size == 2 && message[0] == 'HELO'
132
+ return false
133
+ end
134
+ opts = message[1]
135
+ @shared_key_nonce = opts['nonce'] || '' # make shared_key_check failed (instead of error) if protocol version mismatch exist
136
+ @authentication = opts['auth']
137
+ @allow_keepalive = opts['keepalive']
138
+ true
139
+ end
140
+
141
+ def generate_ping
142
+ log.debug "generating ping"
143
+ # ['PING', self_hostname, sharedkey\_salt, sha512\_hex(sharedkey\_salt + self_hostname + nonce + shared_key),
144
+ # username || '', sha512\_hex(auth\_salt + username + password) || '']
145
+ shared_key_hexdigest = Digest::SHA512.new.update(@shared_key_salt).update(@sender.self_hostname).update(@shared_key_nonce).update(@shared_key).hexdigest
146
+ ping = ['PING', @sender.self_hostname, @shared_key_salt, shared_key_hexdigest]
147
+ if @authentication != ''
148
+ password_hexdigest = Digest::SHA512.new.update(@authentication).update(@username).update(@password).hexdigest
149
+ ping.push(@username, password_hexdigest)
150
+ else
151
+ ping.push('','')
152
+ end
153
+ ping
154
+ end
155
+
156
+ def check_pong(message)
157
+ log.debug "checking pong"
158
+ # ['PONG', bool(authentication result), 'reason if authentication failed',
159
+ # self_hostname, sha512\_hex(salt + self_hostname + nonce + sharedkey)]
160
+ unless message.size == 5 && message[0] == 'PONG'
161
+ return false, 'invalid format for PONG message'
162
+ end
163
+ pong, auth_result, reason, hostname, shared_key_hexdigest = message
164
+
165
+ unless auth_result
166
+ return false, 'authentication failed: ' + reason
167
+ end
168
+
169
+ if hostname == @sender.self_hostname
170
+ return false, 'same hostname between input and output: invalid configuration'
171
+ end
172
+
173
+ clientside = Digest::SHA512.new.update(@shared_key_salt).update(hostname).update(@shared_key_nonce).update(@shared_key).hexdigest
174
+ unless shared_key_hexdigest == clientside
175
+ return false, 'shared key mismatch'
176
+ end
177
+
178
+ return true, nil
179
+ end
180
+
181
+ def send_data(data)
182
+ @sslsession.write data.to_msgpack
183
+ end
184
+
185
+ def on_read(data)
186
+ log.debug "on_read"
187
+ if self.established?
188
+ #TODO: ACK
189
+ log.warn "unknown packets arrived..."
190
+ return
191
+ end
192
+
193
+ case @state
194
+ when :helo
195
+ unless check_helo(data)
196
+ log.warn "received invalid helo message from #{@host}"
197
+ self.shutdown
198
+ return
199
+ end
200
+ send_data generate_ping()
201
+ @state = :pingpong
202
+ when :pingpong
203
+ success, reason = check_pong(data)
204
+ unless success
205
+ log.warn "connection refused to #{@host}:" + reason
206
+ self.shutdown
207
+ return
208
+ end
209
+ log.info "connection established to #{@host}" if @first_session
210
+ @state = :established
211
+ @expire = Time.now + @keepalive if @keepalive && @keepalive > 0
212
+ log.debug "connection established", host: @host, port: @port, expire: @expire
213
+ end
214
+ end
215
+
216
+ def connect
217
+ Thread.current.abort_on_exception = true
218
+ log.debug "starting client"
219
+
220
+ addr = @sender.hostname_resolver.getaddress(@host)
221
+ log.debug "create tcp socket to node", host: @host, address: addr, port: @port
222
+
223
+ begin
224
+ if @proxy_uri.nil? then
225
+ sock = TCPSocket.new(addr, @port)
226
+ else
227
+ proxy = Proxifier::Proxy(@proxy_uri)
228
+ sock = proxy.open(addr, @port)
229
+ end
230
+ rescue => e
231
+ log.warn "failed to connect for secure-forward", error_class: e.class, error: e, host: @host, address: addr, port: @port
232
+ @state = :failed
233
+ return
234
+ end
235
+
236
+ log.trace "changing socket options"
237
+ opt = [1, @sender.send_timeout.to_i].pack('I!I!') # { int l_onoff; int l_linger; }
238
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, opt)
239
+
240
+ opt = [@sender.send_timeout.to_i, 0].pack('L!L!') # struct timeval
241
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, opt)
242
+
243
+ log.trace "initializing SSL contexts"
244
+
245
+ context = OpenSSL::SSL::SSLContext.new(@sender.ssl_version)
246
+
247
+ log.trace "setting SSL verification options"
248
+
249
+ if @sender.secure
250
+ # inject OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
251
+ # https://bugs.ruby-lang.org/issues/9424
252
+ context.set_params({})
253
+
254
+ if @sender.ssl_ciphers
255
+ context.ciphers = @sender.ssl_ciphers
256
+ else
257
+ ### follow httpclient configuration by nahi
258
+ # OpenSSL 0.9.8 default: "ALL:!ADH:!LOW:!EXP:!MD5:+SSLv2:@STRENGTH"
259
+ context.ciphers = "ALL:!aNULL:!eNULL:!SSLv2" # OpenSSL >1.0.0 default
260
+ end
261
+
262
+ log.trace "set verify_mode VERIFY_PEER"
263
+ context.verify_mode = OpenSSL::SSL::VERIFY_PEER
264
+ if @sender.enable_strict_verification
265
+ context.cert_store = OpenSSL::X509::Store.new
266
+ begin
267
+ context.cert_store.set_default_paths
268
+ rescue OpenSSL::X509::StoreError => e
269
+ log.warn "faild to load system default certificates", error: e
270
+ end
271
+ end
272
+ if @sender.ca_cert_path
273
+ log.trace "set to use private CA", path: @sender.ca_cert_path
274
+ context.ca_file = @sender.ca_cert_path
275
+ end
276
+ end
277
+
278
+ log.debug "trying to connect ssl session", host: @host, address: addr, port: @port
279
+ begin
280
+ sslsession = OpenSSL::SSL::SSLSocket.new(sock, context)
281
+ log.trace "connecting...", host: @host, address: addr, port: @port
282
+ sslsession.connect
283
+ rescue => e
284
+ log.warn "failed to establish SSL connection", error_class: e.class, error: e, host: @host, address: addr, port: @port
285
+ @state = :failed
286
+ return
287
+ end
288
+
289
+ log.debug "ssl session connected", host: @host, port: @port
290
+
291
+ begin
292
+ if @sender.enable_strict_verification
293
+ log.debug "checking peer's certificate", subject: sslsession.peer_cert.subject
294
+ sslsession.post_connection_check(@hostlabel)
295
+ verify = sslsession.verify_result
296
+ if verify != OpenSSL::X509::V_OK
297
+ err_name = Fluent::SecureForwardOutput::OpenSSLUtil.verify_result_name(verify)
298
+ log.warn "BUG: failed to verify certification while connecting host #{@host} as #{@hostlabel} (but not raised, why?)"
299
+ log.warn "BUG: verify_result: #{err_name}"
300
+ raise RuntimeError, "BUG: failed to verify certification and to handle it correctly while connecting host #{@host} as #{@hostlabel}"
301
+ end
302
+ end
303
+ rescue OpenSSL::SSL::SSLError => e
304
+ log.warn "failed to verify certification while connecting ssl session", host: @host, hostlabel: @hostlabel
305
+ self.shutdown
306
+ raise
307
+ end
308
+
309
+ log.debug "ssl session connected", host: @host, port: @port
310
+ @socket = sock
311
+ @sslsession = sslsession
312
+
313
+ buf = ''
314
+ read_length = @sender.read_length
315
+ read_interval = @sender.read_interval
316
+ socket_interval = @sender.socket_interval
317
+
318
+ loop do
319
+ break if @detach
320
+
321
+ begin
322
+ while @sslsession.read_nonblock(read_length, buf)
323
+ if buf == ''
324
+ sleep read_interval
325
+ next
326
+ end
327
+ @unpacker.feed_each(buf, &method(:on_read))
328
+ buf = ''
329
+ end
330
+ rescue OpenSSL::SSL::SSLError
331
+ # to wait i/o restart
332
+ sleep socket_interval
333
+ rescue SystemCallError => e
334
+ log.warn "disconnected by Error", error_class: e.class, error: e, host: @host, port: @port
335
+ break
336
+ rescue EOFError
337
+ log.warn "disconnected", host: @host, port: @port
338
+ break
339
+ end
340
+ end
341
+ while @writing
342
+ break if @detach
343
+
344
+ sleep read_interval
345
+ end
346
+ self.shutdown
347
+ end
348
+ end
@@ -0,0 +1,11 @@
1
+ module Fluent
2
+ module Plugin
3
+ module Secure
4
+ module Forward
5
+ module Addproxy
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ require "fluent/plugin/secure/forward/addproxy/version"
2
+
3
+ module Fluent
4
+ module Plugin
5
+ module Secure
6
+ module Forward
7
+ module Addproxy
8
+ # Your code goes here...
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Fluent
2
+ module Plugin
3
+ module Secure
4
+ module Forward
5
+ module V033dev2
6
+ module Addproxy
7
+ VERSION = "0.1.0"
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ require "fluent/plugin/secure/forward/v033dev2/addproxy/version"
2
+
3
+ module Fluent
4
+ module Plugin
5
+ module Secure
6
+ module Forward
7
+ module V033dev2
8
+ module Addproxy
9
+ # Your code goes here...
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end