costan-rtunnel 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/CHANGELOG +13 -0
  2. data/LICENSE +21 -0
  3. data/Manifest +49 -0
  4. data/README.markdown +84 -0
  5. data/Rakefile +45 -0
  6. data/bin/rtunnel_client +4 -0
  7. data/bin/rtunnel_server +4 -0
  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/spec/client_spec.rb +47 -0
  24. data/spec/cmds_spec.rb +127 -0
  25. data/spec/integration_spec.rb +105 -0
  26. data/spec/server_spec.rb +21 -0
  27. data/spec/spec_helper.rb +3 -0
  28. data/test/command_stubs.rb +77 -0
  29. data/test/protocol_mocks.rb +43 -0
  30. data/test/scenario_connection.rb +109 -0
  31. data/test/test_client.rb +48 -0
  32. data/test/test_command_protocol.rb +82 -0
  33. data/test/test_commands.rb +49 -0
  34. data/test/test_connection_id.rb +30 -0
  35. data/test/test_crypto.rb +127 -0
  36. data/test/test_frame_protocol.rb +109 -0
  37. data/test/test_io_extensions.rb +70 -0
  38. data/test/test_server.rb +70 -0
  39. data/test/test_socket_factory.rb +42 -0
  40. data/test/test_tunnel.rb +186 -0
  41. data/test_data/authorized_keys2 +4 -0
  42. data/test_data/known_hosts +4 -0
  43. data/test_data/random_rsa_key +27 -0
  44. data/test_data/ssh_host_dsa_key +12 -0
  45. data/test_data/ssh_host_rsa_key +27 -0
  46. data/tests/_ab_test.rb +16 -0
  47. data/tests/_stress_test.rb +96 -0
  48. data/tests/lo_http_server.rb +55 -0
  49. metadata +121 -0
@@ -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