coderrr-rtunnel 0.3.9 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/CHANGELOG +13 -0
  2. data/LICENSE +21 -0
  3. data/Manifest +48 -0
  4. data/README.markdown +40 -15
  5. data/Rakefile +31 -4
  6. data/bin/rtunnel_client +2 -1
  7. data/bin/rtunnel_server +2 -1
  8. data/lib/rtunnel.rb +20 -0
  9. data/lib/rtunnel/client.rb +308 -0
  10. data/lib/rtunnel/command_processor.rb +62 -0
  11. data/lib/rtunnel/command_protocol.rb +50 -0
  12. data/lib/rtunnel/commands.rb +233 -0
  13. data/lib/rtunnel/connection_id.rb +24 -0
  14. data/lib/rtunnel/core.rb +58 -0
  15. data/lib/rtunnel/crypto.rb +106 -0
  16. data/lib/rtunnel/frame_protocol.rb +34 -0
  17. data/lib/rtunnel/io_extensions.rb +54 -0
  18. data/lib/rtunnel/leak.rb +35 -0
  19. data/lib/rtunnel/rtunnel_client_cmd.rb +41 -0
  20. data/lib/rtunnel/rtunnel_server_cmd.rb +32 -0
  21. data/lib/rtunnel/server.rb +351 -0
  22. data/lib/rtunnel/socket_factory.rb +119 -0
  23. data/test/command_stubs.rb +77 -0
  24. data/test/protocol_mocks.rb +43 -0
  25. data/test/scenario_connection.rb +109 -0
  26. data/test/test_client.rb +48 -0
  27. data/test/test_command_protocol.rb +82 -0
  28. data/test/test_commands.rb +49 -0
  29. data/test/test_connection_id.rb +30 -0
  30. data/test/test_crypto.rb +127 -0
  31. data/test/test_frame_protocol.rb +109 -0
  32. data/test/test_io_extensions.rb +70 -0
  33. data/test/test_server.rb +70 -0
  34. data/test/test_socket_factory.rb +42 -0
  35. data/test/test_tunnel.rb +186 -0
  36. data/test_data/authorized_keys2 +4 -0
  37. data/test_data/known_hosts +4 -0
  38. data/test_data/random_rsa_key +27 -0
  39. data/test_data/ssh_host_dsa_key +12 -0
  40. data/test_data/ssh_host_rsa_key +27 -0
  41. data/tests/_ab_test.rb +16 -0
  42. data/tests/_stress_test.rb +96 -0
  43. data/tests/lo_http_server.rb +55 -0
  44. metadata +67 -27
  45. data/ab_test.rb +0 -23
  46. data/lib/client.rb +0 -150
  47. data/lib/cmds.rb +0 -166
  48. data/lib/core.rb +0 -58
  49. data/lib/rtunnel_client_cmd.rb +0 -23
  50. data/lib/rtunnel_server_cmd.rb +0 -18
  51. data/lib/server.rb +0 -197
  52. data/rtunnel.gemspec +0 -18
  53. data/rtunnel_client.rb +0 -3
  54. data/rtunnel_server.rb +0 -3
  55. data/stress_test.rb +0 -68
@@ -0,0 +1,34 @@
1
+ # eventmachine protocol
2
+ module RTunnel::FrameProtocol
3
+ def receive_data(data)
4
+ @frame_size_buffer ||= ''
5
+
6
+ i = 0
7
+ loop do
8
+ while @frame_buffer.nil? and i < data.size
9
+ @frame_size_buffer << data[i]
10
+ if (data[i] & 0x80) == 0
11
+ @remaining_frame_size = StringIO.new(@frame_size_buffer).read_varsize
12
+ @frame_buffer = ''
13
+ end
14
+ i += 1
15
+ end
16
+
17
+ return if @frame_buffer.nil?
18
+ break if @remaining_frame_size > data.size - i
19
+
20
+ receive_frame(@frame_buffer + data[i, @remaining_frame_size])
21
+ @frame_size_buffer, @frame_buffer = '', nil
22
+ i += @remaining_frame_size
23
+ end
24
+
25
+ @frame_buffer << data[i..-1]
26
+ @remaining_frame_size -= data.size-i
27
+ end
28
+
29
+ def send_frame(frame_data)
30
+ size_str = StringIO.new
31
+ size_str.write_varsize(frame_data.length)
32
+ send_data(size_str.string + frame_data)
33
+ end
34
+ end
@@ -0,0 +1,54 @@
1
+ require 'stringio'
2
+
3
+ class RTunnel::TruncatedDataError < Exception; end
4
+
5
+ module RTunnel::IOExtensions
6
+ # writes a size (non-negative Integer) to the stream using a varint encoding
7
+ def write_varsize(size)
8
+ chars = []
9
+ loop do
10
+ size, char = size.divmod(0x80)
11
+ chars << (char | ((size > 0) ? 0x80 : 0))
12
+ break if size == 0
13
+ end
14
+ write chars.pack('C*')
15
+ end
16
+
17
+ # reads a size (non-negative Integer) from the stream using a varint encoding
18
+ def read_varsize
19
+ size = 0
20
+ multiplier = 1
21
+ loop do
22
+ char = getc
23
+ # TODO(costan): better exception
24
+ unless char
25
+ raise RTunnel::TruncatedDataError.new("Encoded varsize truncated")
26
+ end
27
+ more, size_add = char.divmod(0x80)
28
+ size += size_add * multiplier
29
+ break if more == 0
30
+ multiplier *= 0x80
31
+ end
32
+ size
33
+ end
34
+
35
+ # writes a string and its length, so it can later be read with read_varstr
36
+ def write_varstring(str)
37
+ write_varsize str.length
38
+ write str
39
+ end
40
+
41
+ # reads a variable-length string that was previously written with write_varstr
42
+ def read_varstring
43
+ length = read_varsize
44
+ return '' if length == 0
45
+ str = read(length)
46
+ if ! str or str.length != length
47
+ raise RTunnel::TruncatedDataError, "Encoded varstring truncated"
48
+ end
49
+ str
50
+ end
51
+ end
52
+
53
+ IO.send :include, RTunnel::IOExtensions
54
+ StringIO.send :include, RTunnel::IOExtensions
@@ -0,0 +1,35 @@
1
+ require 'pp'
2
+
3
+ class RTunnel::LeakTracker
4
+ # TODO(not_me): update this to eventmachine if it's still interesting
5
+ def self.start
6
+ logged_thread do
7
+ sleep 10
8
+ begin
9
+ objects = Hash.new 0
10
+
11
+ while true
12
+ last_objects = objects.dup
13
+ ObjectSpace.each_object do |o|
14
+ objects[o.class] += 1
15
+ end
16
+
17
+ objects.reject!{|k,v| ! last_objects.has_key? k } unless last_objects.empty?
18
+
19
+ new_objects = objects.dup
20
+ objects.each do |(klass, count)|
21
+ new_objects.delete klass if count < last_objects[klass] # has been GC'ed, "cant be leaking"
22
+ end
23
+ objects = new_objects
24
+
25
+ PP.pp objects.sort_by{|(k,cnt)| cnt }.reverse[0..10], STDERR
26
+
27
+ sleep 10
28
+ end
29
+ rescue Object
30
+ STDERR.puts $!.inspect
31
+ STDERR.puts $!.backtrace.join("\n")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ require 'optparse'
2
+
3
+ require 'rubygems'
4
+ require 'eventmachine'
5
+
6
+
7
+ module RTunnel
8
+ def self.run_client
9
+ options = {}
10
+
11
+ (opts = OptionParser.new do |o|
12
+ o.on("-c", "--control-address ADDRESS") do |a|
13
+ options[:control_address] = a
14
+ end
15
+ o.on("-f", "--remote-listen-port ADDRESS") do |a|
16
+ options[:remote_listen_address] = a
17
+ end
18
+ o.on("-t", "--tunnel-to ADDRESS") do |a|
19
+ options[:tunnel_to_address] = a
20
+ end
21
+ o.on("-k", "--private-key KEYFILE") do |f|
22
+ options[:private_key] = f
23
+ end
24
+ o.on("-l", "--log-level LEVEL") do |l|
25
+ options[:log_level] = l
26
+ end
27
+ o.on("-o", "--timeout TIMEOUT_IN_SECONDS") do |t|
28
+ options[:tunnel_timeout] = t.to_f
29
+ end
30
+ end).parse! rescue (puts opts; return)
31
+
32
+ mandatory_keys = [:control_address, :remote_listen_address,
33
+ :tunnel_to_address]
34
+
35
+ (puts opts; return) unless mandatory_keys.all? { |key| options[key] }
36
+
37
+ EventMachine::run do
38
+ RTunnel::Client.new(options).start
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ require 'optparse'
2
+
3
+ require 'rubygems'
4
+ require 'eventmachine'
5
+
6
+
7
+ module RTunnel
8
+ def self.run_server
9
+ options = {}
10
+
11
+ (opts = OptionParser.new do |o|
12
+ o.on("-c", "--control ADDRESS") { |a| options[:control_address] = a }
13
+ o.on("-a", "--authorized-keys KEYSFILE") do |f|
14
+ options[:authorized_keys] = f
15
+ end
16
+ o.on("-l", "--log-level LEVEL") { |l| options[:log_level] = l }
17
+ o.on("-k", "--keep-alive KEEP_ALIVE_INTERVAL") do |t|
18
+ options[:keep_alive_interval] = t.to_f
19
+ end
20
+ o.on("-p", "--lowest-listen-port PORT") do |p|
21
+ options[:lowest_listen_port] = p.to_i
22
+ end
23
+ o.on("-P", "--highest-listen-port PORT") do |p|
24
+ options[:highest_listen_port] = p.to_i
25
+ end
26
+ end).parse! rescue (puts opts; return)
27
+
28
+ EventMachine::run do
29
+ RTunnel::Server.new(options).start
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,351 @@
1
+ require 'set'
2
+
3
+ require 'rubygems'
4
+ require 'eventmachine'
5
+
6
+
7
+ # The RTunnel server class, managing control and connection servers.
8
+ class RTunnel::Server
9
+ include RTunnel
10
+ include RTunnel::CommandProcessor
11
+ include RTunnel::Logging
12
+ include RTunnel::ConnectionId
13
+
14
+ attr_reader :control_address, :control_host, :control_port
15
+ attr_reader :keep_alive_interval, :authorized_keys
16
+ attr_reader :lowest_listen_port, :highest_listen_port
17
+ attr_reader :tunnel_connections
18
+
19
+ def initialize(options = {})
20
+ process_options options
21
+ @tunnel_controls = {}
22
+ @tunnel_connections = {}
23
+ @tunnel_connections_by_control = {}
24
+ end
25
+
26
+ def start
27
+ return if @control_listener
28
+ @control_host = SocketFactory.host_from_address @control_address
29
+ @control_port = SocketFactory.port_from_address @control_address
30
+ start_server
31
+ end
32
+
33
+ def stop
34
+ return unless @control_listener
35
+ EventMachine.stop_server @control_listener
36
+ @control_listener = nil
37
+ end
38
+
39
+ def start_server
40
+ D "Control server on #{@control_host} port #{@control_port}"
41
+ @control_listener = EventMachine.start_server @control_host, @control_port,
42
+ Server::ControlConnection,
43
+ self
44
+ end
45
+
46
+ # Creates a listener on a certain port. The given block should create and
47
+ # return the listener. If a listener is already active on the given port,
48
+ # the current listener is closed, and the new listener is created after the
49
+ # old listener is closed.
50
+ def create_tunnel_listener(listen_port, control_connection, &creation_block)
51
+ if old_control = @tunnel_controls[listen_port]
52
+ D "Closing old listener on port #{listen_port}"
53
+ EventMachine.stop_server old_control.listener
54
+ end
55
+
56
+ EventMachine.next_tick do
57
+ next unless yield
58
+
59
+ @tunnel_controls[listen_port] = control_connection
60
+ redirect_tunnel_connections old_control, control_connection if old_control
61
+ on_remote_listen
62
+ end
63
+ end
64
+
65
+ # Registers a tunnel connection, so it can receive data.
66
+ def register_tunnel_connection(connection)
67
+ @tunnel_connections[connection.connection_id] = connection
68
+ control_connection = connection.control_connection
69
+ @tunnel_connections_by_control[control_connection] ||= Set.new
70
+ @tunnel_connections_by_control[control_connection] << connection
71
+ end
72
+
73
+ # De-registers a tunnel connection.
74
+ def deregister_tunnel_connection(connection)
75
+ @tunnel_connections.delete connection.connection_id
76
+ control_connection = connection.control_connection
77
+ @tunnel_connections_by_control[control_connection].delete connection
78
+ end
79
+
80
+ def redirect_tunnel_connections(old_control, new_control)
81
+ return unless old_connections = @tunnel_connections_by_control[old_control]
82
+ old_connections.each do |tunnel_connection|
83
+ tunnel_connection.control_connection = new_control
84
+ end
85
+ @tunnel_connections_by_control[new_control] ||= Set.new
86
+ @tunnel_connections_by_control[new_control] += old_connections
87
+ end
88
+
89
+ def on_remote_listen(&block)
90
+ if block
91
+ @on_remote_listen = block
92
+ elsif @on_remote_listen
93
+ @on_remote_listen.call
94
+ end
95
+ end
96
+
97
+ ## option processing
98
+
99
+ def process_options(options)
100
+ [:control_address, :keep_alive_interval, :authorized_keys,
101
+ :lowest_listen_port, :highest_listen_port].each do |opt|
102
+ instance_variable_set "@#{opt}".to_sym,
103
+ RTunnel::Server.send("extract_#{opt}".to_sym, options[opt])
104
+ end
105
+
106
+ init_log :level => options[:log_level]
107
+ end
108
+
109
+ def self.extract_control_address(address)
110
+ return "0.0.0.0:#{RTunnel::DEFAULT_CONTROL_PORT}" unless address
111
+ if address =~ /^\d+$/
112
+ host = nil
113
+ port = address.to_i
114
+ else
115
+ host = SocketFactory.host_from_address address
116
+ port = SocketFactory.port_from_address address
117
+ end
118
+ host = RTunnel.resolve_address(host || "0.0.0.0")
119
+ port ||= RTunnel::DEFAULT_CONTROL_PORT.to_s
120
+ "#{host}:#{port}"
121
+ end
122
+
123
+ def self.extract_keep_alive_interval(interval)
124
+ interval || RTunnel::KEEP_ALIVE_INTERVAL
125
+ end
126
+
127
+ def self.extract_authorized_keys(keys_file)
128
+ keys_file and Crypto.load_public_keys keys_file
129
+ end
130
+
131
+ def self.extract_lowest_listen_port(port)
132
+ port || 0
133
+ end
134
+
135
+ def self.extract_highest_listen_port(port)
136
+ port || 65535
137
+ end
138
+ end
139
+
140
+
141
+ # A client connection to the server's control port.
142
+ class RTunnel::Server::ControlConnection < EventMachine::Connection
143
+ include RTunnel
144
+ include RTunnel::CommandProcessor
145
+ include RTunnel::CommandProtocol
146
+ include RTunnel::Logging
147
+
148
+ attr_reader :server, :listener
149
+
150
+ def initialize(server)
151
+ super()
152
+
153
+ @server = server
154
+ @tunnel_connections = server.tunnel_connections
155
+ @listener = nil
156
+ @keep_alive_timer = nil
157
+ @keep_alive_interval = server.keep_alive_interval
158
+ @in_hasher = @out_hasher = nil
159
+
160
+ init_log :to => @server
161
+ end
162
+
163
+ def post_init
164
+ @client_port, @client_host = *Socket.unpack_sockaddr_in(get_peername)
165
+ D "Established connection with #{@client_host} port #{@client_port}"
166
+ enable_keep_alives
167
+ end
168
+
169
+ def unbind
170
+ D "Lost connection from #{@client_host} port #{@client_port}"
171
+ disable_keep_alives
172
+ end
173
+
174
+
175
+ ## Command processing
176
+
177
+ def process_remote_listen(address)
178
+ listen_host = SocketFactory.host_from_address address
179
+ listen_port = SocketFactory.port_from_address address
180
+
181
+ unless validate_remote_listen listen_host, listen_port
182
+ send_command SetSessionKeyCommand.new('NO')
183
+ return
184
+ end
185
+
186
+ @server.create_tunnel_listener listen_port, self do
187
+ D "Creating listener for #{listen_host} port #{listen_port}"
188
+ begin
189
+ @listener = EventMachine.start_server listen_host, listen_port,
190
+ Server::TunnelConnection, self,
191
+ listen_host, listen_port
192
+ rescue RuntimeError => e
193
+ # EventMachine raises 'no acceptor' if the listen address is invalid
194
+ E "Invalid listen address #{listen_host}"
195
+ @listener = nil
196
+ end
197
+ end
198
+
199
+ D "Listening on #{listen_host} port #{listen_port}"
200
+ end
201
+
202
+ # Verifies if a RemoteListenCommand should be honored.
203
+ def validate_remote_listen(host, port)
204
+ if @server.authorized_keys and @out_hasher.nil?
205
+ D "Asked to open listen socket by unauthorized client"
206
+ return false
207
+ end
208
+
209
+ if port < @server.lowest_listen_port or port > @server.highest_listen_port
210
+ D "Asked to listen to forbidden port"
211
+ return false
212
+ end
213
+
214
+ true
215
+ end
216
+
217
+ def process_send_data(tunnel_connection_id, data)
218
+ tunnel_connection = @tunnel_connections[tunnel_connection_id]
219
+ if tunnel_connection
220
+ D "Data: #{data.length} bytes coming from #{tunnel_connection_id}"
221
+ tunnel_connection.send_data data
222
+ else
223
+ W "Asked to send to unknown connection #{tunnel_connection_id}"
224
+ end
225
+ end
226
+
227
+ def process_close_connection(tunnel_connection_id)
228
+ tunnel_connection = @tunnel_connections[tunnel_connection_id]
229
+ if tunnel_connection
230
+ D "Closed from tunneled end: #{tunnel_connection_id}"
231
+ tunnel_connection.close_from_tunnel
232
+ else
233
+ W "Asked to close unknown connection #{tunnel_connection_id}"
234
+ end
235
+ end
236
+
237
+ def process_generate_session_key(public_key_fp)
238
+ if @server.authorized_keys
239
+ if public_key = @server.authorized_keys[public_key_fp]
240
+ D "Authorized client key received, generating session key"
241
+ @out_hasher, @in_hasher = Crypto::Hasher.new, Crypto::Hasher.new
242
+
243
+ iokeys = StringIO.new
244
+ iokeys.write_varstring @in_hasher.key
245
+ iokeys.write_varstring @out_hasher.key
246
+ encrypted_keys = Crypto.encrypt_with_key public_key, iokeys.string
247
+ else
248
+ D("Rejecting unauthorized client key (%s authorized keys)" %
249
+ @server.authorized_keys.length)
250
+ encrypted_keys = 'NO'
251
+ end
252
+ else
253
+ D "Asked to generate session key, but no authorized keys set"
254
+ encrypted_keys = ''
255
+ end
256
+ send_command SetSessionKeyCommand.new(encrypted_keys)
257
+ self.incoming_command_hasher = @in_hasher if @in_hasher
258
+ self.outgoing_command_hasher = @out_hasher if @out_hasher
259
+ end
260
+
261
+ def receive_bad_frame(frame, exception)
262
+ case exception
263
+ when :bad_signature
264
+ D "Ignoring command with invalid signature"
265
+ when Exception
266
+ D "Ignoring malformed command."
267
+ D "Decoding exception: #{exception.class.name} - #{exception}\n" +
268
+ "#{exception.backtrace.join("\n")}\n"
269
+ end
270
+ end
271
+
272
+
273
+ ## Keep-Alives (preventing timeouts)
274
+
275
+ #:nodoc:
276
+ def send_command(command)
277
+ @last_command_time = Time.now
278
+ super
279
+ end
280
+
281
+ # Enables sending KeepAliveCommands every few seconds.
282
+ def enable_keep_alives
283
+ @last_command_time = Time.now
284
+ @keep_alive_timer =
285
+ EventMachine::PeriodicTimer.new(@keep_alive_interval / 2) do
286
+ keep_alive_if_needed
287
+ end
288
+ end
289
+
290
+ # Sends a KeepAlive command if no command was sent recently.
291
+ def keep_alive_if_needed
292
+ if Time.now - @last_command_time >= @keep_alive_interval
293
+ send_command KeepAliveCommand.new
294
+ end
295
+ end
296
+
297
+ # Disables sending KeepAlives.
298
+ def disable_keep_alives
299
+ return unless @keep_alive_timer
300
+ @keep_alive_timer.cancel
301
+ @keep_alive_timer = nil
302
+ end
303
+ end
304
+
305
+
306
+ # A connection to a tunnelled port.
307
+ class RTunnel::Server::TunnelConnection < EventMachine::Connection
308
+ include RTunnel
309
+ include RTunnel::Logging
310
+
311
+ attr_reader :connection_id
312
+ attr_accessor :control_connection
313
+
314
+ def initialize(control_connection, listen_host, listen_port)
315
+ # listen_host and listen_port are passed for logging purposes only
316
+ @listen_host = listen_host
317
+ @listen_port = listen_port
318
+ @control_connection = control_connection
319
+ @server = @control_connection.server
320
+ @hasher = nil
321
+
322
+ init_log :to => @server
323
+ end
324
+
325
+ def post_init
326
+ @connection_id = @server.new_connection_id
327
+ peer = Socket.unpack_sockaddr_in(get_peername).reverse.join ':'
328
+ D "Tunnel connection from #{peer} on #{@connection_id}"
329
+ @server.register_tunnel_connection self
330
+ @control_connection.send_command CreateConnectionCommand.new(@connection_id)
331
+ end
332
+
333
+ def unbind
334
+ unless @tunnel_closed
335
+ D "Closed from client end: #{@connection_id}"
336
+ close_command = CloseConnectionCommand.new(@connection_id)
337
+ @control_connection.send_command close_command
338
+ end
339
+ @server.deregister_tunnel_connection self
340
+ end
341
+
342
+ def close_from_tunnel
343
+ @tunnel_closed = true
344
+ close_connection_after_writing
345
+ end
346
+
347
+ def receive_data(data)
348
+ D "Data: #{data.length} bytes for #{@connection_id}"
349
+ @control_connection.send_command SendDataCommand.new(@connection_id, data)
350
+ end
351
+ end