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
data/CHANGELOG ADDED
@@ -0,0 +1,13 @@
1
+ v0.4.0. Re-packaged as gem using echoe. Eventmachine. Client authentication.
2
+
3
+ v0.3.9. Cleanup and removed dependencies.
4
+
5
+ v0.3.6. New protocol. Available as gem for easier installation.
6
+
7
+ v0.2.1. Minor bugfix, cmdline options change.
8
+
9
+ v0.2.0. Much simpler
10
+
11
+ v0.1.2. Created rtunnel_server binary for linux so you don't need Ruby installed on the host you want to reverse tunnel from.
12
+
13
+ v0.1.1. Added default control port of 19050, no longer have to specify this on client or server unless you care to change it.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2008 coderrr, Victor Costan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/Manifest ADDED
@@ -0,0 +1,49 @@
1
+ CHANGELOG
2
+ LICENSE
3
+ Manifest
4
+ README.markdown
5
+ Rakefile
6
+ bin/rtunnel_client
7
+ bin/rtunnel_server
8
+ lib/rtunnel.rb
9
+ lib/rtunnel/client.rb
10
+ lib/rtunnel/command_protocol.rb
11
+ lib/rtunnel/commands.rb
12
+ lib/rtunnel/core.rb
13
+ lib/rtunnel/crypto.rb
14
+ lib/rtunnel/frame_protocol.rb
15
+ lib/rtunnel/io_extensions.rb
16
+ lib/rtunnel/leak.rb
17
+ lib/rtunnel/rtunnel_client_cmd.rb
18
+ lib/rtunnel/rtunnel_server_cmd.rb
19
+ lib/rtunnel/server.rb
20
+ lib/rtunnel/socket_factory.rb
21
+ lib/rtunnel/connection_id.rb
22
+ lib/rtunnel/command_processor.rb
23
+ rtunnel.gemspec
24
+ spec/client_spec.rb
25
+ spec/cmds_spec.rb
26
+ spec/integration_spec.rb
27
+ spec/server_spec.rb
28
+ spec/spec_helper.rb
29
+ test/command_stubs.rb
30
+ test/protocol_mocks.rb
31
+ test/scenario_connection.rb
32
+ test/test_client.rb
33
+ test/test_command_protocol.rb
34
+ test/test_commands.rb
35
+ test/test_crypto.rb
36
+ test/test_frame_protocol.rb
37
+ test/test_io_extensions.rb
38
+ test/test_server.rb
39
+ test/test_socket_factory.rb
40
+ test/test_tunnel.rb
41
+ test/test_connection_id.rb
42
+ test_data/known_hosts
43
+ test_data/ssh_host_rsa_key
44
+ test_data/random_rsa_key
45
+ test_data/ssh_host_dsa_key
46
+ test_data/authorized_keys2
47
+ tests/_ab_test.rb
48
+ tests/_stress_test.rb
49
+ tests/lo_http_server.rb
data/README.markdown ADDED
@@ -0,0 +1,84 @@
1
+ INSTALL
2
+ -
3
+
4
+ on server and local machine:
5
+
6
+ `sudo gem sources -a http://gems.github.com`
7
+
8
+ `sudo gem install coderrr-rtunnel`
9
+
10
+ If you don't have root access you can run the above commands without `sudo` and rubygems will install the gem into your `~/.gem` directory. If you go this route, make sure you add the gems executable dir to your path.
11
+
12
+ `export PATH=$PATH:~/.gem/ruby/1.8/bin`
13
+
14
+ USAGE
15
+ -
16
+
17
+ on server (myserver.com):
18
+
19
+ `rtunnel_server`
20
+
21
+ on your local machine:
22
+
23
+ `rtunnel_client -c myserver.com -f 4000 -t 3000`
24
+
25
+ This would reverse tunnel myserver.com:4000 to localhost:3000 so that if you had a web server running at port 3000 on your local machine, anyone on the internet could access it by going to http://myserver.com:4000
26
+
27
+ **Logging (verbosity) level**
28
+
29
+ Both the server and the client support 4 levels of logging - 'debug', 'info', 'warn', 'error'. The -l parameter sets the logging level. The default level is 'error'. For example:
30
+
31
+ `rtunnel_server -l debug`
32
+
33
+ starts a server that will output debugging information.
34
+
35
+ **Secure connections**
36
+
37
+ RTunnel can be configured to use ssh keys to control access to the server. A ssh
38
+ key (generated by ssh-genkey) is required on the client, and the server must
39
+ have a list of authorized keys (using the format of known_hosts.) The keys are
40
+ used to authenticate clients and guarantee data integrity. For performance
41
+ reasons, encryption is not done.
42
+
43
+ Server setup:
44
+
45
+ `rtunnel_server -a ~/.ssh/known_hosts`
46
+
47
+ Client setup:
48
+
49
+ `rtunnel_client -c myserver.com -f 4000 -t 3000 -k /etc/ssh/ssh_host_rsa_key`
50
+
51
+ If you're concerned about security, you probably want to restrict the range of
52
+ ports that clients can open up on the rtunnel server.
53
+
54
+ `rtunnel_server -p 3000 -P 3999`
55
+
56
+ restricts clients to using ports 3000-3999 for reverse tunnels.
57
+
58
+
59
+ <a name="about">RTunnel?</a>
60
+ -
61
+
62
+ This client/server allow you to reverse tunnel traffic. Reverse tunneling is useful if you want to run a server behind a NAT and you do not have the ability to use port forwarding. The specific reason I created this program was to reduce the pain of Facebook App development on a crappy internet connection that drops often. ssh -R was not cutting it.
63
+
64
+ **How does reverse tunneling work?**
65
+
66
+ * tunnel\_client makes connection to tunnel\_server (through NAT)
67
+ * tunnel_server listens on port X
68
+ * internet_user connects to port X on tunnel server
69
+ * tunnel\_server uses existing connection to tunnel internet\_user's request back to tunnel\_client
70
+ * tunnel_client connects to local server on port Y
71
+ * tunnel_client tunnels internet\_user's connection through to local server
72
+
73
+ or:
74
+
75
+ * establish connection: tunnel\_client --NAT--> tunnel\_server
76
+ * reverse tunnel: internet\_user -> tunnel_server --(NAT)--> tunnel\_client -> server\_running\_behind\_nat
77
+
78
+ **How is this different than normal tunneling?**
79
+
80
+ With tunneling, usually your connections are made in the same direction you create the tunnel connection. With reverse tunneling, you tunnel your connections the opposite direction of which you made the tunnel connection. So you initiate the tunnel with A -> B, but connections are tunneled from B -> A.
81
+
82
+ **Why not just use ssh -R?**
83
+
84
+ The same thing can be achieved with ssh -R, so why not just use it? A lot of ssh servers don't have the GatewayPorts sshd option set up to allow you to reverse tunnel. If you are not in control of the server and it is not setup correctly then you are SOL. RTunnel does not require you are in control of the server. ssh -R also has other annoyances. When your connection drops and you try to re-initiate the reverse tunnel sometimes you get an 'address already in use error' because the old tunnel process is still laying around. This may require you to kill the existing sshd process. RTunnel does not have this problem.
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'echoe'
3
+ require 'rake'
4
+ require 'spec/rake/spectask'
5
+
6
+ $: << File.join(File.dirname(__FILE__), 'lib')
7
+ require 'rtunnel'
8
+
9
+ desc "Run all examples"
10
+ Spec::Rake::SpecTask.new('spec') do |t|
11
+ t.spec_files = FileList['spec/**/*.rb']
12
+ end
13
+
14
+ desc "Create packages"
15
+ task :pkg4google do
16
+ system %Q{cd .. && tar cvzf rtunnel/pkg/rtunnel-#{RTunnel::VERSION}.tar.gz \
17
+ rtunnel --exclude=.svn --exclude=pkg --exclude=rtunnel.ipr \
18
+ --exclude=rtunnel.iws --exclude=rtunnel.iml
19
+ }
20
+ system "rubyscript2exe rtunnel_server.rb --stop-immediately &&
21
+ mv rtunnel_server_linux pkg/rtunnel_server_linux-#{RTunnel::VERSION}"
22
+ end
23
+
24
+ desc "Print command codes"
25
+ task :codes do
26
+ print RTunnel::Command.printable_codes
27
+ end
28
+
29
+ Echoe.new('rtunnel') do |p|
30
+ p.rubyforge_name = 'coderrr'
31
+ p.author = 'coderrr'
32
+ p.email = 'coderrr.contact@gmail.com'
33
+ p.summary = 'Reverse tunnel server and client.'
34
+ p.description = ''
35
+ p.url = 'http://github.com/coderrr/rtunnel'
36
+ # p.remote_rdoc_dir = '' # Release to root
37
+ p.dependencies = ["eventmachine >=0.12.2",
38
+ "net-ssh >=2.0.4"]
39
+ p.development_dependencies = ["echoe >=3.0.1",
40
+ "rspec >=1.1.11",
41
+ "simple-daemon >=0.1.2",
42
+ "thin >=1.0.0"]
43
+ p.need_tar_gz = false
44
+ p.need_zip = false
45
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'rtunnel'
4
+ RTunnel.run_client
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'rtunnel'
4
+ RTunnel.run_server
data/lib/rtunnel.rb ADDED
@@ -0,0 +1,20 @@
1
+ module RTunnel
2
+ end
3
+
4
+ require 'rtunnel/core.rb'
5
+ require 'rtunnel/io_extensions.rb'
6
+ require 'rtunnel/socket_factory.rb'
7
+ require 'rtunnel/frame_protocol.rb'
8
+
9
+ require 'rtunnel/commands.rb'
10
+ require 'rtunnel/command_processor.rb'
11
+ require 'rtunnel/command_protocol.rb'
12
+ require 'rtunnel/connection_id.rb'
13
+ require 'rtunnel/crypto.rb'
14
+
15
+ require 'rtunnel/client.rb'
16
+ require 'rtunnel/leak.rb'
17
+ require 'rtunnel/server.rb'
18
+
19
+ require 'rtunnel/rtunnel_client_cmd.rb'
20
+ require 'rtunnel/rtunnel_server_cmd.rb'
@@ -0,0 +1,308 @@
1
+ require 'resolv'
2
+ require 'timeout'
3
+
4
+ require 'rubygems'
5
+ require 'eventmachine'
6
+
7
+ class RTunnel::Client
8
+ include RTunnel
9
+ include RTunnel::Logging
10
+
11
+ attr_reader :control_address, :control_host, :control_port
12
+ attr_reader :remote_listen_address, :tunnel_to_address
13
+ attr_reader :tunnel_timeout, :private_key
14
+ attr_reader :logger
15
+ attr_reader :connections, :server_connection
16
+
17
+ def initialize(options = {})
18
+ process_options options
19
+ @connections = {}
20
+ @server_connection = nil
21
+ end
22
+
23
+ def start
24
+ return if @server_connection
25
+ @control_host = SocketFactory.host_from_address @control_address
26
+ @control_port = SocketFactory.port_from_address @control_address
27
+ connect_to_server
28
+ end
29
+
30
+ def connect_to_server
31
+ D "Connecting to #{@control_host} port #{@control_port}"
32
+ @server_connection = EventMachine.connect @control_host, @control_port,
33
+ Client::ServerConnection, self
34
+ end
35
+
36
+ def stop
37
+ @connections.each { |connection| connection.close_connection_after_writing }
38
+
39
+ return unless @server_connection
40
+ @server_connection.close_connection_after_writing
41
+ @server_connection.disable_tunnel_timeouts
42
+ @server_connection = nil
43
+ end
44
+
45
+ ## option processing
46
+
47
+ def process_options(options)
48
+ [:control_address, :remote_listen_address, :tunnel_to_address,
49
+ :tunnel_timeout, :private_key].each do |opt|
50
+ instance_variable_set "@#{opt}".to_sym,
51
+ RTunnel::Client.send("extract_#{opt}".to_sym, options[opt])
52
+ end
53
+
54
+ init_log :level => options[:log_level]
55
+ end
56
+
57
+ def self.extract_control_address(address)
58
+ unless SocketFactory.port_from_address address
59
+ address = "#{address}:#{RTunnel::DEFAULT_CONTROL_PORT}"
60
+ end
61
+ RTunnel.resolve_address address
62
+ end
63
+
64
+ def self.extract_remote_listen_address(address)
65
+ unless SocketFactory.port_from_address address
66
+ address = "0.0.0.0:#{address}"
67
+ end
68
+ RTunnel.resolve_address address
69
+ end
70
+
71
+ def self.extract_tunnel_to_address(address)
72
+ address = "localhost:#{address}" if address =~ /^\d+$/
73
+ RTunnel.resolve_address address
74
+ end
75
+
76
+ def self.extract_tunnel_timeout(timeout)
77
+ timeout || RTunnel::TUNNEL_TIMEOUT
78
+ end
79
+
80
+ def self.extract_private_key(key_file)
81
+ key_file and Crypto.read_private_key key_file
82
+ end
83
+ end
84
+
85
+ # Connection to the server's control port.
86
+ class RTunnel::Client::ServerConnection < EventMachine::Connection
87
+ # Note: I would've loved to make this a module, but event_machine's
88
+ # connection init order (initialize, connect block, post_init) does not
89
+ # work as advertised (the connect block seems to execute after post_init).
90
+ # So I'm taking the safe route and having my own initialize.
91
+
92
+ include RTunnel
93
+ include RTunnel::Logging
94
+ include RTunnel::CommandProcessor
95
+ include RTunnel::CommandProtocol
96
+
97
+ attr_reader :client
98
+
99
+ def initialize(client)
100
+ super()
101
+
102
+ @client = client
103
+ @tunnel_to_address = client.tunnel_to_address
104
+ @tunnel_to_host = SocketFactory.host_from_address @tunnel_to_address
105
+ @tunnel_to_port = SocketFactory.port_from_address @tunnel_to_address
106
+ @timeout_timer = nil
107
+ @hasher = nil
108
+ @connections = @client.connections
109
+ init_log :to => @client
110
+ end
111
+
112
+ def post_init
113
+ if @client.private_key
114
+ request_session_key
115
+ else
116
+ request_listen
117
+ end
118
+ end
119
+
120
+ # Asks the server to open a listen socket for this client's tunnel.
121
+ def request_listen
122
+ send_command RemoteListenCommand.new(@client.remote_listen_address)
123
+ enable_tunnel_timeouts
124
+ end
125
+
126
+ # Asks the server to establish a session key with this client.
127
+ def request_session_key
128
+ D 'Private key provided, asking server for session key'
129
+ key_fp = Crypto.key_fingerprint @client.private_key
130
+ send_command GenerateSessionKeyCommand.new(key_fp)
131
+ end
132
+
133
+ def unbind
134
+ # wait for a second, then try connecting again
135
+ W 'Lost server connection, will reconnect in 1s'
136
+ EventMachine.add_timer(1.0) { client.connect_to_server }
137
+ @connections.each { |conn_id, conn| conn.close_connection_after_writing }
138
+ @connections.clear
139
+ end
140
+
141
+
142
+ ## Command processing
143
+
144
+ # CreateConnectionCommand handler
145
+ def process_create_connection(connection_id)
146
+ if @connections[connection_id]
147
+ E "asked to create already open connection #{connection_id}"
148
+ return
149
+ end
150
+
151
+ D "Tunnel #{connection_id} to #{@tunnel_to_host} port #{@tunnel_to_port}"
152
+ connection = EventMachine.connect(@tunnel_to_host, @tunnel_to_port,
153
+ Client::TunnelConnection, connection_id, @client)
154
+ @connections[connection_id] = connection
155
+ end
156
+
157
+ # CloseConnectionCommand handler
158
+ def process_close_connection(connection_id)
159
+ if connection = @connections[connection_id]
160
+ I "Closing connection #{connection_id}"
161
+ connection.close_connection_after_writing
162
+ @connections.delete connection_id
163
+ else
164
+ W "Asked to close inexistent connection #{connection_id}"
165
+ end
166
+ end
167
+ # Called when a tunnel connection is closed.
168
+ def data_connection_closed(connection_id)
169
+ return unless @connections.delete(connection_id)
170
+ D "Connection #{connection_id} closed by this end"
171
+ send_command CloseConnectionCommand.new(connection_id)
172
+ end
173
+
174
+ # SendData handler
175
+ def process_send_data(connection_id, data)
176
+ if connection = @connections[connection_id]
177
+ D "Data: #{data.length} bytes for #{connection_id}"
178
+ connection.tunnel_data data
179
+ else
180
+ W "Received data for non-existent connection #{connection_id}!"
181
+ end
182
+ end
183
+
184
+ # SetSessionKey handler
185
+ def process_set_session_key(encrypted_keys)
186
+ case encrypted_keys
187
+ when ''
188
+ W "Sent key to open tunnel server"
189
+ request_listen
190
+ when 'NO'
191
+ if @client.private_key
192
+ E "Server refused provided key"
193
+ else
194
+ E "Server requires authentication and no private key was provided"
195
+ end
196
+ close_connection_after_writing
197
+ else
198
+ D "Received server session keys, installing hashers"
199
+ iokeys = StringIO.new Crypto.decrypt_with_key(client.private_key,
200
+ encrypted_keys)
201
+ @out_hasher = Crypto::Hasher.new iokeys.read_varstring
202
+ @in_hasher = Crypto::Hasher.new iokeys.read_varstring
203
+ self.outgoing_command_hasher = @out_hasher
204
+ self.incoming_command_hasher = @in_hasher
205
+
206
+ D "Hashers installed, opening listen socket on server"
207
+ request_listen
208
+ end
209
+ end
210
+
211
+ def receive_bad_frame(frame, exception)
212
+ case exception
213
+ when :bad_signature
214
+ D "Ignoring command with invalid signature"
215
+ when Exception
216
+ D "Ignoring malformed command."
217
+ D "Decoding exception: #{exception.class.name} - #{exception}\n" +
218
+ "#{exception.backtrace.join("\n")}\n"
219
+ end
220
+ end
221
+
222
+ ## Connection timeouts.
223
+
224
+ # Keep-alive received from the control connection.
225
+ def process_keep_alive
226
+ end
227
+
228
+ #:nodoc:
229
+ def receive_command(command)
230
+ @last_packet_time = Time.now
231
+ super
232
+ end
233
+
234
+ # After this is called, the control connection will be closed if no command is
235
+ # received within a certain amount of time.
236
+ def enable_tunnel_timeouts
237
+ @last_packet_time = Time.now
238
+ @timeout_timer = EventMachine::PeriodicTimer.new(1.0) do
239
+ check_tunnel_timeout
240
+ end
241
+ end
242
+
243
+ # Closes the connection if no command has been received for some time.
244
+ def check_tunnel_timeout
245
+ if tunnel_timeout?
246
+ W 'Tunnel timeout. Disconnecting from server.'
247
+ disable_tunnel_timeouts
248
+ close_connection_after_writing
249
+ end
250
+ end
251
+
252
+ # Disables timeout checking (so the tunnel will not be torn down if no command
253
+ # is received for some period of time).
254
+ def disable_tunnel_timeouts
255
+ return unless @timeout_timer
256
+ @timeout_timer.cancel
257
+ @timeout_timer = nil
258
+ end
259
+
260
+ # If true, a tunnel timeout has occured.
261
+ def tunnel_timeout?
262
+ Time.now - @last_packet_time > client.tunnel_timeout
263
+ end
264
+ end
265
+
266
+ # A connection to the tunnelled port.
267
+ class RTunnel::Client::TunnelConnection < EventMachine::Connection
268
+ include RTunnel
269
+ include RTunnel::Logging
270
+
271
+ def initialize(connection_id, client)
272
+ super()
273
+
274
+ @connection_id = connection_id
275
+ @backlog = ''
276
+ @client = client
277
+ init_log :to => @client
278
+ end
279
+
280
+ def server_connection
281
+ @client.server_connection
282
+ end
283
+
284
+ def tunnel_data(data)
285
+ # if the connection hasn't been accepted, store the incoming data until
286
+ # sending can happen
287
+ if @backlog
288
+ @backlog << data
289
+ else
290
+ send_data data
291
+ end
292
+ end
293
+
294
+ def post_init
295
+ D "Tunnel #{@connection_id} established"
296
+ send_data @backlog unless @backlog.empty?
297
+ @backlog = nil
298
+ end
299
+
300
+ def receive_data(data)
301
+ D "Data: #{data.length} bytes from #{@connection_id}"
302
+ server_connection.send_command SendDataCommand.new(@connection_id, data)
303
+ end
304
+
305
+ def unbind
306
+ server_connection.data_connection_closed @connection_id
307
+ end
308
+ end