coderrr-rtunnel 0.3.9 → 0.4.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.
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,62 @@
1
+ # The plumbing for processing RTunnel commands.
2
+ module RTunnel::CommandProcessor
3
+ # Called by CommandProtocol to process a command.
4
+ def receive_command(command)
5
+ case @last_command = command
6
+ when RTunnel::CloseConnectionCommand
7
+ process_close_connection command.connection_id
8
+ when RTunnel::CreateConnectionCommand
9
+ process_create_connection command.connection_id
10
+ when RTunnel::GenerateSessionKeyCommand
11
+ process_generate_session_key command.public_key_fp
12
+ when RTunnel::KeepAliveCommand
13
+ process_keep_alive
14
+ when RTunnel::RemoteListenCommand
15
+ process_remote_listen command.address
16
+ when RTunnel::SendDataCommand
17
+ process_send_data command.connection_id, command.data
18
+ when RTunnel::SetSessionKeyCommand
19
+ process_set_session_key command.encrypted_keys
20
+ end
21
+ end
22
+
23
+ # Override to process CloseConnectionCommand. Do NOT call super.
24
+ def process_close_connection(connection_id)
25
+ unexpected_command @last_command
26
+ end
27
+
28
+ # Override to process CreateConnectionCommand. Do NOT call super.
29
+ def process_create_connection(connection_id)
30
+ unexpected_command @last_command
31
+ end
32
+
33
+ # Override to process GenerateSessionKeyCommand. Do NOT call super.
34
+ def process_generate_session_key(public_key_fp)
35
+ unexpected_command @last_command
36
+ end
37
+
38
+ # Override to process KeepAliveCommand. Do NOT call super.
39
+ def process_keep_alive
40
+ unexpected_command @last_command
41
+ end
42
+
43
+ # Override to process RemoteListenCommand. Do NOT call super.
44
+ def process_remote_listen(address)
45
+ unexpected_command @last_command
46
+ end
47
+
48
+ # Override to process SendDataCommand. Do NOT call super.
49
+ def process_send_data(connection_id, data)
50
+ unexpected_command @last_command
51
+ end
52
+
53
+ # Override to process SetSessionKeyCommand. Do NOT call super.
54
+ def process_set_session_key(encrypted_keys)
55
+ unexpected_command @last_command
56
+ end
57
+
58
+ # Override to handle commands that haven't been overridden.
59
+ def unexpected_command(command)
60
+ W "Unexpected command: #{command.inspect}"
61
+ end
62
+ end
@@ -0,0 +1,50 @@
1
+ module RTunnel::CommandProtocol
2
+ include RTunnel::FrameProtocol
3
+
4
+ # Sends an encoded RTunnel command as a frame.
5
+ def send_command(command)
6
+ command_str = command.to_encoded_str
7
+ if @out_command_hasher
8
+ send_frame command_str + @out_command_hasher.hash(command_str)
9
+ else
10
+ send_frame command_str
11
+ end
12
+ end
13
+
14
+ # Decodes a frame into an RTunnel command.
15
+ def receive_frame(frame)
16
+ ioframe = StringIO.new frame
17
+ begin
18
+ command = RTunnel::Command.decode ioframe
19
+ rescue Exception => e
20
+ receive_bad_frame frame, e
21
+ return
22
+ end
23
+ if @in_command_hasher
24
+ signature = ioframe.read
25
+ if signature != @in_command_hasher.hash(frame[0...(-signature.length)])
26
+ receive_bad_frame frame, :bad_signature
27
+ return
28
+ end
29
+ end
30
+ receive_command command
31
+ end
32
+
33
+ # Sets a cryptographic hasher that will be used to sign outgoing commands.
34
+ # Once a hasher is set, all outgoing frames will be signed.
35
+ def outgoing_command_hasher=(hasher)
36
+ @out_command_hasher = hasher
37
+ end
38
+
39
+ # Sets a cryptographic hasher that will be used to verify incoming commands.
40
+ # Once a hasher is set, all incoming frames without a matching signature will
41
+ # be ignored.
42
+ def incoming_command_hasher=(hasher)
43
+ @in_command_hasher = hasher
44
+ end
45
+
46
+ # Override to handle frames with corrupted or absent signatures.
47
+ def receive_bad_frame(frame, exception)
48
+ nil
49
+ end
50
+ end
@@ -0,0 +1,233 @@
1
+ require 'stringio'
2
+
3
+ class RTunnel::Command
4
+ # Associates command codes with the classes implementing them.
5
+ class Registry
6
+ def initialize
7
+ @classes = {}
8
+ @codes = {}
9
+ end
10
+
11
+ def register(klass, command_code)
12
+ if @codes.has_key? command_code
13
+ raise "Command code #{command_code} already used for #{@codes[command_code].name}"
14
+ end
15
+
16
+ @codes[klass] = command_code
17
+ @classes[command_code] = klass
18
+ end
19
+
20
+ def class_for(command_code)
21
+ @classes[command_code]
22
+ end
23
+
24
+ def code_for(klass)
25
+ @codes[klass]
26
+ end
27
+
28
+ def codes_and_classes
29
+ ret_val = []
30
+ @codes.each { |klass, code| ret_val << [code, klass.name] }
31
+ ret_val.sort!
32
+ end
33
+ end
34
+
35
+ @@registry = Registry.new
36
+ def self.registry
37
+ @@registry
38
+ end
39
+
40
+ # subclasses must call this to register and declare their command code
41
+ def self.command_code(code)
42
+ registry.register self, code
43
+ end
44
+
45
+ # subclasses should override this (and add to the result)
46
+ # to provide a debug string
47
+ def to_s
48
+ self.class.name
49
+ end
50
+
51
+ # subclasses should override this and call super
52
+ # before performing their own initialization
53
+ def initialize_from_io(io)
54
+ return self
55
+ end
56
+
57
+ # Encode this command to a IO / IOString.
58
+ def encode(io)
59
+ io.write RTunnel::Command.registry.code_for(self.class)
60
+ end
61
+
62
+ # Produce a string with an encoding of this command.
63
+ def to_encoded_str
64
+ string_io = StringIO.new
65
+ self.encode string_io
66
+ string_io.string
67
+ end
68
+
69
+ # Decode a Command instance from a IO / IOString.
70
+ def self.decode(io)
71
+ return nil unless code = io.getc
72
+ klass = registry.class_for code.chr
73
+ return nil unless klass
74
+
75
+ command = klass.new
76
+ command.initialize_from_io io
77
+ end
78
+
79
+ # Printable string containing all the codes and their classes.
80
+ def self.printable_codes
81
+ printable = ''
82
+ registry.codes_and_classes.each do |code_and_class|
83
+ printable << "#{code_and_class.first}: #{code_and_class.last}\n"
84
+ end
85
+ return printable
86
+ end
87
+ end
88
+
89
+ class RTunnel::ConnectionCommand < RTunnel::Command
90
+ attr_reader :connection_id
91
+
92
+ def initialize(connection_id = nil)
93
+ super()
94
+ @connection_id = connection_id
95
+ end
96
+
97
+ def to_s
98
+ super + "/id=#{connection_id}"
99
+ end
100
+
101
+ def initialize_from_io(io)
102
+ super
103
+ @connection_id = io.read_varstring
104
+ self
105
+ end
106
+
107
+ def encode(io)
108
+ super
109
+ io.write_varstring @connection_id
110
+ end
111
+ end
112
+
113
+ class RTunnel::CreateConnectionCommand < RTunnel::ConnectionCommand
114
+ command_code 'C'
115
+ end
116
+
117
+ class RTunnel::CloseConnectionCommand < RTunnel::ConnectionCommand
118
+ command_code 'X'
119
+ end
120
+
121
+ class RTunnel::SendDataCommand < RTunnel::ConnectionCommand
122
+ command_code 'D'
123
+
124
+ attr_reader :data
125
+
126
+ def initialize(connection_id = nil, data = nil)
127
+ super(connection_id)
128
+ @data = data
129
+ end
130
+
131
+ def initialize_from_io(io)
132
+ super
133
+ @data = io.read_varstring
134
+ self
135
+ end
136
+
137
+ def to_s
138
+ super + "/data=#{data}"
139
+ end
140
+
141
+ def encode(io)
142
+ super
143
+ io.write_varstring @data
144
+ end
145
+ end
146
+
147
+ class RTunnel::RemoteListenCommand < RTunnel::Command
148
+ command_code 'L'
149
+
150
+ attr_reader :address
151
+
152
+ def initialize(address = nil)
153
+ super()
154
+ @address = address
155
+ end
156
+
157
+ def to_s
158
+ super + "/address=#{address}"
159
+ end
160
+
161
+ def initialize_from_io(io)
162
+ super
163
+ @address = io.read_varstring
164
+ self
165
+ end
166
+
167
+ def encode(io)
168
+ super
169
+ io.write_varstring address
170
+ end
171
+ end
172
+
173
+ class RTunnel::KeepAliveCommand < RTunnel::Command
174
+ command_code 'A'
175
+
176
+ def initialize_from_io(io)
177
+ super
178
+ end
179
+ end
180
+
181
+ class RTunnel::GenerateSessionKeyCommand < RTunnel::Command
182
+ command_code 'S'
183
+
184
+ attr_reader :public_key_fp
185
+
186
+ def initialize(public_key_fp = nil)
187
+ super()
188
+ @public_key_fp = public_key_fp
189
+ end
190
+
191
+ def to_s
192
+ super + "/pubkey_fp=#{@public_key_fp.inspect}"
193
+ end
194
+
195
+ def initialize_from_io(io)
196
+ super
197
+ @public_key_fp = io.read_varstring
198
+ self
199
+ end
200
+
201
+ def encode(io)
202
+ super
203
+ io.write_varstring @public_key_fp
204
+ end
205
+ end
206
+
207
+ class RTunnel::SetSessionKeyCommand < RTunnel::Command
208
+ command_code 'K'
209
+
210
+ attr_reader :encrypted_keys
211
+
212
+ def initialize(encrypted_keys = nil)
213
+ super()
214
+ @encrypted_keys = encrypted_keys
215
+ end
216
+
217
+ def to_s
218
+ super + "/enc_keys=#{@encrypted_key.inspect}"
219
+ end
220
+
221
+ def initialize_from_io(io)
222
+ super
223
+ @encrypted_keys = io.read_varstring
224
+ self
225
+ end
226
+
227
+ def encode(io)
228
+ super
229
+ io.write_varstring @encrypted_keys
230
+ end
231
+ end
232
+
233
+ # TODO(not_me): this file (and its tests) cry for a DSL
@@ -0,0 +1,24 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+
4
+ # Unique ID generation functionality.
5
+ module RTunnel::ConnectionId
6
+ def self.new_cipher
7
+ cipher = OpenSSL::Cipher::Cipher.new 'aes-128-ecb'
8
+ cipher.encrypt
9
+ cipher.key, cipher.iv = cipher.random_key, cipher.random_iv
10
+ cipher
11
+ end
12
+
13
+ def self.new_counter
14
+ '0' * 16
15
+ end
16
+
17
+ def new_connection_id
18
+ @session_id_cipher ||= RTunnel::ConnectionId.new_cipher
19
+ @session_id_counter ||= RTunnel::ConnectionId.new_counter
20
+ connection_id = @session_id_cipher.update @session_id_counter
21
+ @session_id_counter.succ!
22
+ Base64.encode64(connection_id).strip
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ require 'logger'
2
+
3
+ module RTunnel
4
+ DEFAULT_CONTROL_PORT = 19050
5
+ TUNNEL_TIMEOUT = 10
6
+ KEEP_ALIVE_INTERVAL = 2
7
+
8
+ class AbortProgramException < Exception
9
+
10
+ end
11
+ end
12
+
13
+ module RTunnel::Logging
14
+ def init_log(options = {})
15
+ # TODO(costan): parse logging options
16
+ if options[:to]
17
+ @log = options[:to].instance_variable_get(:@log).dup
18
+ else
19
+ @log = Logger.new(STDERR)
20
+ @log.level = Logger::ERROR
21
+ end
22
+ if options[:level]
23
+ @log.level = Logger::const_get(options[:level].upcase.to_sym)
24
+ end
25
+ end
26
+
27
+ def D(message)
28
+ @log.debug message
29
+ end
30
+
31
+ def W(message)
32
+ @log.warn message
33
+ end
34
+
35
+ def I(message)
36
+ @log.info message
37
+ end
38
+
39
+ def E(message)
40
+ @log.error message
41
+ end
42
+
43
+ def F(message)
44
+ @log.fatal message
45
+ end
46
+ end
47
+
48
+ module RTunnel
49
+ # Resolve the given address to an IP.
50
+ # The address can have the following formats: host; host:port; ip; ip:port;
51
+ def self.resolve_address(address, timeout_sec = 5)
52
+ host, rest = address.split(':', 2)
53
+ ip = timeout(timeout_sec) { Resolv.getaddress(host) }
54
+ rest ? "#{ip}:#{rest}" : ip
55
+ rescue Exception
56
+ raise AbortProgramException, "Error resolving #{host}"
57
+ end
58
+ end
@@ -0,0 +1,106 @@
1
+ require 'digest/sha2'
2
+ require 'openssl'
3
+ require 'stringio'
4
+
5
+ require 'rubygems'
6
+ require 'net/ssh'
7
+
8
+ module RTunnel::Crypto
9
+ # Reads all the keys from an openssh known_hosts or authorized_keys2 file.
10
+ def self.read_authorized_keys(file_name)
11
+ keys = []
12
+ File.read(file_name).each_line do |line|
13
+ pubkey_match = /ssh-\w*\s*(\S*)/.match line
14
+ next unless pubkey_match
15
+ pubkey_blob = pubkey_match[1].unpack('m*').first
16
+ keys << Net::SSH::Buffer.new(pubkey_blob).read_key
17
+ end
18
+ keys
19
+ end
20
+
21
+ # Loads a private key from an openssh key file.
22
+ def self.read_private_key(file_name)
23
+ Net::SSH::KeyFactory.load_private_key file_name
24
+ end
25
+
26
+ # Computes a string that represents the key. Different keys should
27
+ # map out to different fingerprints.
28
+ def self.key_fingerprint(key)
29
+ key.public_key.to_der
30
+ end
31
+
32
+ # Encrypts some data with a public key. The matching private key will be
33
+ # required to decrypt the data.
34
+ def self.encrypt_with_key(key, data)
35
+ if key.kind_of? OpenSSL::PKey::RSA
36
+ key.public_encrypt data, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
37
+ elsif key.kind_of? OpenSSL::PKey::DSA
38
+ key.public_encrypt encrypted_data
39
+ else
40
+ raise 'Unsupported key type'
41
+ end
42
+ end
43
+
44
+ # Decrypts data that was previously encrypted with encrypt_with_key.
45
+ def self.decrypt_with_key(key, encrypted_data)
46
+ if key.kind_of? OpenSSL::PKey::RSA
47
+ key.private_decrypt encrypted_data, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
48
+ elsif key.kind_of? OpenSSL::PKey::DSA
49
+ key.private_decrypt encrypted_data
50
+ else
51
+ raise 'Unsupported key type'
52
+ end
53
+ end
54
+
55
+ # Loads public keys to be used by a server.
56
+ def self.load_public_keys(file_name)
57
+ key_list = read_authorized_keys file_name
58
+ RTunnel::Crypto::KeySet.new key_list
59
+ end
60
+ end
61
+
62
+ # A set of keys used by a server to authenticate clients.
63
+ class RTunnel::Crypto::KeySet
64
+ def initialize(key_list)
65
+ @keys_by_fp = {}
66
+ key_list.each { |k| @keys_by_fp[RTunnel::Crypto.key_fingerprint(k)] = k }
67
+ end
68
+
69
+ def [](key_fp)
70
+ @keys_by_fp[key_fp]
71
+ end
72
+
73
+ def length
74
+ @keys_by_fp.length
75
+ end
76
+ end
77
+
78
+ # A cryptographically secure hasher. Instances will hash the data
79
+ class RTunnel::Crypto::Hasher
80
+ attr_reader :key
81
+
82
+ def initialize(key = nil)
83
+ @key = key || RTunnel::Crypto::Hasher.random_key
84
+ @cipher = OpenSSL::Cipher::Cipher.new 'aes-128-cbc'
85
+ @cipher.encrypt
86
+ iokey = StringIO.new @key
87
+ @cipher.key = iokey.read_varstring
88
+ @cipher.iv = iokey.read_varstring
89
+ end
90
+
91
+ # Creates a hash for the given data. Warning: this method is not idempotent.
92
+ # The intent is that the same hash can be produced by another hasher that is
93
+ # initialized with the same key and has been fed the same data.
94
+ def hash(data)
95
+ @cipher.update Digest::SHA2.digest(data)
96
+ end
97
+
98
+ # Produces a random key for the hasher.
99
+ def self.random_key
100
+ cipher = OpenSSL::Cipher::Cipher.new 'aes-128-cbc'
101
+ iokey = StringIO.new
102
+ iokey.write_varstring cipher.random_key
103
+ iokey.write_varstring cipher.random_iv
104
+ iokey.string
105
+ end
106
+ end