rtunnel 0.3.8 → 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 (61) hide show
  1. data/CHANGELOG +13 -0
  2. data/LICENSE +21 -0
  3. data/Manifest +48 -0
  4. data/README.markdown +84 -0
  5. data/Rakefile +45 -0
  6. data/bin/rtunnel_client +2 -1
  7. data/bin/rtunnel_server +2 -1
  8. data/lib/rtunnel/client.rb +308 -0
  9. data/lib/rtunnel/command_processor.rb +62 -0
  10. data/lib/rtunnel/command_protocol.rb +50 -0
  11. data/lib/rtunnel/commands.rb +233 -0
  12. data/lib/rtunnel/connection_id.rb +24 -0
  13. data/lib/rtunnel/core.rb +58 -0
  14. data/lib/rtunnel/crypto.rb +106 -0
  15. data/lib/rtunnel/frame_protocol.rb +34 -0
  16. data/lib/rtunnel/io_extensions.rb +54 -0
  17. data/lib/rtunnel/leak.rb +35 -0
  18. data/lib/rtunnel/rtunnel_client_cmd.rb +41 -0
  19. data/lib/rtunnel/rtunnel_server_cmd.rb +32 -0
  20. data/lib/rtunnel/server.rb +351 -0
  21. data/lib/rtunnel/socket_factory.rb +119 -0
  22. data/lib/rtunnel.rb +20 -0
  23. data/rtunnel.gemspec +51 -0
  24. data/spec/client_spec.rb +47 -0
  25. data/spec/cmds_spec.rb +127 -0
  26. data/spec/integration_spec.rb +105 -0
  27. data/spec/server_spec.rb +21 -0
  28. data/spec/spec_helper.rb +3 -0
  29. data/test/command_stubs.rb +77 -0
  30. data/test/protocol_mocks.rb +43 -0
  31. data/test/scenario_connection.rb +109 -0
  32. data/test/test_client.rb +48 -0
  33. data/test/test_command_protocol.rb +82 -0
  34. data/test/test_commands.rb +49 -0
  35. data/test/test_connection_id.rb +30 -0
  36. data/test/test_crypto.rb +127 -0
  37. data/test/test_frame_protocol.rb +109 -0
  38. data/test/test_io_extensions.rb +70 -0
  39. data/test/test_server.rb +70 -0
  40. data/test/test_socket_factory.rb +42 -0
  41. data/test/test_tunnel.rb +186 -0
  42. data/test_data/authorized_keys2 +4 -0
  43. data/test_data/known_hosts +4 -0
  44. data/test_data/random_rsa_key +27 -0
  45. data/test_data/ssh_host_dsa_key +12 -0
  46. data/test_data/ssh_host_rsa_key +27 -0
  47. data/tests/_ab_test.rb +16 -0
  48. data/tests/_stress_test.rb +96 -0
  49. data/tests/lo_http_server.rb +55 -0
  50. metadata +127 -31
  51. data/History.txt +0 -3
  52. data/Manifest.txt +0 -13
  53. data/README.txt +0 -362
  54. data/lib/client.rb +0 -185
  55. data/lib/cmds.rb +0 -166
  56. data/lib/core.rb +0 -53
  57. data/lib/rtunnel_client_cmd.rb +0 -25
  58. data/lib/rtunnel_server_cmd.rb +0 -20
  59. data/lib/server.rb +0 -181
  60. data/rtunnel_client.rb +0 -3
  61. data/rtunnel_server.rb +0 -3
@@ -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
@@ -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