coderrr-rtunnel 0.3.8
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +59 -0
- data/Rakefile +18 -0
- data/ab_test.rb +23 -0
- data/bin/rtunnel_client +3 -0
- data/bin/rtunnel_server +3 -0
- data/lib/client.rb +156 -0
- data/lib/cmds.rb +166 -0
- data/lib/core.rb +53 -0
- data/lib/rtunnel_client_cmd.rb +25 -0
- data/lib/rtunnel_server_cmd.rb +20 -0
- data/lib/server.rb +199 -0
- data/rtunnel.gemspec +18 -0
- data/rtunnel_client.rb +3 -0
- data/rtunnel_server.rb +3 -0
- data/spec/client_spec.rb +47 -0
- data/spec/cmds_spec.rb +127 -0
- data/spec/integration_spec.rb +105 -0
- data/spec/server_spec.rb +21 -0
- data/spec/spec_helper.rb +3 -0
- data/stress_test.rb +74 -0
- metadata +79 -0
data/README.markdown
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
INSTALL
|
2
|
+
-
|
3
|
+
|
4
|
+
on server and local machine:
|
5
|
+
|
6
|
+
`gem install rtunnel`
|
7
|
+
|
8
|
+
If you don't have root access on server, you can use either the rtunnel_server_linux binary (only works with linux), or extract the .tar.gz and use `rtunnel_server.rb` (all function the same)
|
9
|
+
|
10
|
+
USAGE
|
11
|
+
-
|
12
|
+
|
13
|
+
on server (myserver.com):
|
14
|
+
|
15
|
+
`rtunnel_server`
|
16
|
+
|
17
|
+
on your local machine:
|
18
|
+
|
19
|
+
`rtunnel_client -c myserver.com -f 4000 -t 3000`
|
20
|
+
|
21
|
+
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
|
22
|
+
|
23
|
+
**News**
|
24
|
+
|
25
|
+
* 0.3.6 released, new protocol
|
26
|
+
* created gem for easier installation
|
27
|
+
* 0.2.1 released, minor bugfix, cmdline options change
|
28
|
+
* 0.2.0 released, much simpler
|
29
|
+
* 0.1.2 released
|
30
|
+
* Created rtunnel_server binary for linux so you don't need Ruby installed on the host you want to reverse tunnel from
|
31
|
+
* 0.1.1 released
|
32
|
+
* Added default control port of 19050, no longer have to specify this on client or server unless you care to change it
|
33
|
+
|
34
|
+
RTunnel?
|
35
|
+
-
|
36
|
+
|
37
|
+
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 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.
|
38
|
+
|
39
|
+
**How does reverse tunneling work?**
|
40
|
+
|
41
|
+
* tunnel\_client makes connection to tunnel\_server (through NAT)
|
42
|
+
* tunnel_server listens on port X
|
43
|
+
* internet_user connects to port X on tunnel server
|
44
|
+
* tunnel\_server uses existing connection to tunnel internet user's request back to tunnel\_client
|
45
|
+
* tunnel_client connects to local server on port Y
|
46
|
+
* tunnel_client tunnels internet users connection through to local server
|
47
|
+
|
48
|
+
or:
|
49
|
+
|
50
|
+
* establish connection: tunnel\_client --NAT--> tunnel\_server
|
51
|
+
* reverse tunnel: internet\_user -> tunnel_server --(NAT)--> tunnel\_client -> server\_running\_behind\_nat
|
52
|
+
|
53
|
+
**How is this different than normal tunneling?**
|
54
|
+
|
55
|
+
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.
|
56
|
+
|
57
|
+
**Why not just use ssh -R?**
|
58
|
+
|
59
|
+
The same thing can be achieved with ssh -R, 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 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 requires you to kill the existing sshd process. RTunnel does not have this problem.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
require 'lib/core'
|
4
|
+
|
5
|
+
desc "Run all examples"
|
6
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
7
|
+
t.spec_files = FileList['spec/**/*.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Create packages"
|
11
|
+
task :pkg4google do
|
12
|
+
system %Q{cd .. && tar cvzf rtunnel/pkg/rtunnel-#{RTunnel::VERSION}.tar.gz rtunnel \
|
13
|
+
--exclude=.svn --exclude=pkg --exclude=rtunnel.ipr --exclude=rtunnel.iws \
|
14
|
+
--exclude=rtunnel.iml
|
15
|
+
}
|
16
|
+
system "rubyscript2exe rtunnel_server.rb --stop-immediately &&
|
17
|
+
mv rtunnel_server_linux pkg/rtunnel_server_linux-#{RTunnel::VERSION}"
|
18
|
+
end
|
data/ab_test.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#require 'facets'
|
2
|
+
require 'net/http'
|
3
|
+
require 'logger'
|
4
|
+
require 'stringio'
|
5
|
+
require 'webrick'
|
6
|
+
|
7
|
+
require 'lib/core'
|
8
|
+
|
9
|
+
puts "make sure a fast responding webserver is started on port 4000"
|
10
|
+
|
11
|
+
base_dir = File.dirname(__FILE__)
|
12
|
+
|
13
|
+
fork{ exec "ruby #{base_dir}/rtunnel_server.rb > /dev/null 2>&1" }
|
14
|
+
fork{ exec "ruby #{base_dir}/rtunnel_client.rb -c localhost -f 5000 -t 4000 > /dev/null" }
|
15
|
+
|
16
|
+
sleep 2
|
17
|
+
|
18
|
+
(puts "you need ab (apache bench) to run this stress test" ; exit) if %x{which ab}.empty?
|
19
|
+
|
20
|
+
Process.wait fork { exec("ab -c 500 -n 10000 http://localhost:5000/") }
|
21
|
+
|
22
|
+
puts "done, hit ^C"
|
23
|
+
sleep 999999
|
data/bin/rtunnel_client
ADDED
data/bin/rtunnel_server
ADDED
data/lib/client.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'gserver'
|
4
|
+
require 'optparse'
|
5
|
+
require 'timeout'
|
6
|
+
require 'resolv'
|
7
|
+
|
8
|
+
require 'core'
|
9
|
+
require 'cmds'
|
10
|
+
require 'leak'
|
11
|
+
|
12
|
+
$debug = true
|
13
|
+
|
14
|
+
module RTunnel
|
15
|
+
class Client
|
16
|
+
CONNECTIONS = {}
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
@control_address = options[:control_address]
|
20
|
+
@control_address << ":#{DEFAULT_CONTROL_PORT}" if @control_address !~ /:\d+$/
|
21
|
+
@control_host = @control_address.split(/:/).first
|
22
|
+
|
23
|
+
@remote_listen_address = options[:remote_listen_address]
|
24
|
+
@remote_listen_address.insert 0, "0.0.0.0:" if @remote_listen_address =~ /^\d+$/
|
25
|
+
|
26
|
+
@tunnel_to_address = options[:tunnel_to_address]
|
27
|
+
@tunnel_to_address.insert 0, "localhost:" if @tunnel_to_address =~ /^\d+$/
|
28
|
+
|
29
|
+
[@control_address, @remote_listen_address, @tunnel_to_address].each do |addr|
|
30
|
+
addr.replace_with_ip!
|
31
|
+
end
|
32
|
+
|
33
|
+
@ping_timeout = options[:ping_timeout] || PING_TIMEOUT
|
34
|
+
end
|
35
|
+
|
36
|
+
def start
|
37
|
+
@threads = []
|
38
|
+
|
39
|
+
@last_ping = Time.now
|
40
|
+
|
41
|
+
@threads << Thread.safe do
|
42
|
+
loop do
|
43
|
+
if @check_ping and (Time.now - @last_ping) > @ping_timeout
|
44
|
+
D "control connection timeout"
|
45
|
+
@control_sock.close rescue nil
|
46
|
+
end
|
47
|
+
|
48
|
+
sleep 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Memory leak testing
|
53
|
+
LeakTracker.start
|
54
|
+
|
55
|
+
@main_thread = Thread.safe do
|
56
|
+
loop do
|
57
|
+
stop_ping_check
|
58
|
+
D "connecting to control address (#{@control_address})"
|
59
|
+
@control_sock = begin
|
60
|
+
timeout(5) { TCPSocket.new(*@control_address.split(/:/)) }
|
61
|
+
rescue Exception
|
62
|
+
D "fail"
|
63
|
+
sleep 1
|
64
|
+
next
|
65
|
+
end
|
66
|
+
|
67
|
+
start_ping_check
|
68
|
+
|
69
|
+
write_to_control_sock RemoteListenCommand.new(@remote_listen_address)
|
70
|
+
|
71
|
+
cmd_queue = ""
|
72
|
+
while data = (@control_sock.readpartial(16384) rescue (D "control sock read error: #{$!.inspect}"; nil))
|
73
|
+
cmd_queue << data
|
74
|
+
while Command.match(cmd_queue)
|
75
|
+
case command = Command.parse(cmd_queue)
|
76
|
+
when PingCommand
|
77
|
+
@last_ping = Time.now
|
78
|
+
when CreateConnectionCommand
|
79
|
+
begin
|
80
|
+
# TODO: this currently blocks, but if we put it in thread, a SendDataCommand may try to get run for this connection before the connection exists
|
81
|
+
CONNECTIONS[command.conn_id] = TCPSocket.new(*@tunnel_to_address.split(/:/))
|
82
|
+
|
83
|
+
Thread.safe do
|
84
|
+
cmd = command
|
85
|
+
conn = CONNECTIONS[cmd.conn_id]
|
86
|
+
|
87
|
+
begin
|
88
|
+
while localdata = conn.readpartial(16834)
|
89
|
+
write_to_control_sock SendDataCommand.new(cmd.conn_id, localdata)
|
90
|
+
end
|
91
|
+
rescue Exception
|
92
|
+
begin
|
93
|
+
D "to tunnel closed, closing from tunnel"
|
94
|
+
conn.close
|
95
|
+
CONNECTIONS.delete cmd.conn_id
|
96
|
+
write_to_control_sock CloseConnectionCommand.new(cmd.conn_id)
|
97
|
+
rescue
|
98
|
+
p $!
|
99
|
+
puts $!.backtrace.join("\n")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
rescue Exception
|
104
|
+
D "error connecting to local port"
|
105
|
+
write_to_control_sock CloseConnectionCommand.new(command.conn_id)
|
106
|
+
end
|
107
|
+
when CloseConnectionCommand
|
108
|
+
D "closing connection #{command.conn_id}"
|
109
|
+
if connection = CONNECTIONS[command.conn_id]
|
110
|
+
# TODO: how the hell do u catch a .close error?
|
111
|
+
connection.close_read
|
112
|
+
#connection.close unless connection.closed?
|
113
|
+
CONNECTIONS.delete(command.conn_id)
|
114
|
+
end
|
115
|
+
when SendDataCommand
|
116
|
+
if connection = CONNECTIONS[command.conn_id]
|
117
|
+
connection.write(command.data)
|
118
|
+
else
|
119
|
+
puts "WARNING: received data for non existant connection!"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
@threads << @main_thread
|
128
|
+
end
|
129
|
+
|
130
|
+
def join
|
131
|
+
@main_thread.join
|
132
|
+
end
|
133
|
+
|
134
|
+
def stop
|
135
|
+
@threads.each { |t| t.kill! rescue nil}
|
136
|
+
@control_sock.close rescue nil
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def write_to_control_sock(data)
|
142
|
+
(@control_sock_mutex ||= Mutex.new).synchronize do
|
143
|
+
@control_sock.write data
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def start_ping_check
|
148
|
+
@last_ping = Time.now
|
149
|
+
@check_ping = true
|
150
|
+
end
|
151
|
+
|
152
|
+
def stop_ping_check
|
153
|
+
@check_ping = false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
data/lib/cmds.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
module RTunnel
|
2
|
+
class Command
|
3
|
+
|
4
|
+
def to_s
|
5
|
+
CLASSES_TO_CODES[self.class].dup
|
6
|
+
end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def parse(data)
|
10
|
+
klass = class_from_code(data[0..0])
|
11
|
+
|
12
|
+
new_data = data[1..-1]
|
13
|
+
cmd = klass.parse(new_data)
|
14
|
+
|
15
|
+
data.replace(new_data)
|
16
|
+
|
17
|
+
cmd
|
18
|
+
end
|
19
|
+
|
20
|
+
def match(data)
|
21
|
+
return false if ! (klass = class_from_code(data[0..0]))
|
22
|
+
|
23
|
+
klass.match(data[1..-1])
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def class_from_code(code)
|
29
|
+
CODES_TO_CLASSES[code]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class ConnectionCommand < Command
|
35
|
+
attr_reader :conn_id
|
36
|
+
|
37
|
+
def initialize(conn_id)
|
38
|
+
@conn_id = conn_id
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
super + "#{conn_id}|"
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
RE = %r{^([^|]+)\|}
|
47
|
+
|
48
|
+
def parse(data)
|
49
|
+
data =~ RE
|
50
|
+
conn_id = $1
|
51
|
+
|
52
|
+
cmd = self.new(conn_id)
|
53
|
+
|
54
|
+
data.sub! RE, ''
|
55
|
+
|
56
|
+
cmd
|
57
|
+
end
|
58
|
+
|
59
|
+
def match(data)
|
60
|
+
!! (data =~ RE)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
class CreateConnectionCommand < ConnectionCommand
|
67
|
+
end
|
68
|
+
|
69
|
+
class CloseConnectionCommand < ConnectionCommand
|
70
|
+
end
|
71
|
+
|
72
|
+
class SendDataCommand < Command
|
73
|
+
attr_reader :conn_id
|
74
|
+
attr_reader :data
|
75
|
+
|
76
|
+
def initialize(conn_id, data)
|
77
|
+
@conn_id = conn_id
|
78
|
+
@data = data
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_s
|
82
|
+
super + "#{conn_id}|#{data.size}|#{data}"
|
83
|
+
end
|
84
|
+
|
85
|
+
class << self
|
86
|
+
RE = %r{^([^|]+)\|([^|]+)\|}
|
87
|
+
|
88
|
+
def parse(data)
|
89
|
+
data =~ RE
|
90
|
+
|
91
|
+
conn_id = $1
|
92
|
+
data_size = $2.to_i
|
93
|
+
|
94
|
+
new_data = data.sub(RE, '')
|
95
|
+
cmd_data = new_data[0,data_size]
|
96
|
+
|
97
|
+
cmd = SendDataCommand.new(conn_id, cmd_data)
|
98
|
+
|
99
|
+
data.replace(new_data[data_size..-1])
|
100
|
+
|
101
|
+
cmd
|
102
|
+
end
|
103
|
+
|
104
|
+
def match(data)
|
105
|
+
return false if ! (data =~ RE)
|
106
|
+
|
107
|
+
data_size = $2.to_i
|
108
|
+
|
109
|
+
data.sub(RE, '').size >= data_size
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class RemoteListenCommand < Command
|
115
|
+
attr_reader :address
|
116
|
+
|
117
|
+
def initialize(address)
|
118
|
+
@address = address
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_s
|
122
|
+
super + "#{address}|"
|
123
|
+
end
|
124
|
+
|
125
|
+
class << self
|
126
|
+
RE = %r{^([^|]+)\|}
|
127
|
+
|
128
|
+
def parse(data)
|
129
|
+
data =~ RE
|
130
|
+
address = $1
|
131
|
+
|
132
|
+
cmd = self.new(address)
|
133
|
+
|
134
|
+
data.sub! RE, ''
|
135
|
+
|
136
|
+
cmd
|
137
|
+
end
|
138
|
+
|
139
|
+
def match(data)
|
140
|
+
!! (data =~ RE)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
class PingCommand < Command
|
147
|
+
def self.parse(data)
|
148
|
+
PingCommand.new
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.match(data)
|
152
|
+
true
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class Command
|
157
|
+
CODES_TO_CLASSES = {
|
158
|
+
"C" => CreateConnectionCommand,
|
159
|
+
"X" => CloseConnectionCommand,
|
160
|
+
"D" => SendDataCommand,
|
161
|
+
"P" => PingCommand,
|
162
|
+
"L" => RemoteListenCommand,
|
163
|
+
}
|
164
|
+
CLASSES_TO_CODES = CODES_TO_CLASSES.invert
|
165
|
+
end
|
166
|
+
end
|
data/lib/core.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module RTunnel
|
2
|
+
VERSION = '0.3.8'
|
3
|
+
|
4
|
+
DEFAULT_CONTROL_PORT = 19050
|
5
|
+
PING_TIMEOUT = 10
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
def D(msg)
|
10
|
+
puts msg if $debug
|
11
|
+
end
|
12
|
+
|
13
|
+
class << Thread
|
14
|
+
def safe(*a)
|
15
|
+
Thread.new(*a) do
|
16
|
+
begin
|
17
|
+
yield
|
18
|
+
rescue Exception
|
19
|
+
puts $!.inspect
|
20
|
+
puts $!.backtrace.join("\n")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class String
|
27
|
+
def replace_with_ip!
|
28
|
+
host = self.split(/:/).first
|
29
|
+
|
30
|
+
ip = timeout(5) { Resolv.getaddress(host) }
|
31
|
+
|
32
|
+
self.replace(self.gsub(host, ip))
|
33
|
+
rescue Exception
|
34
|
+
puts "Error resolving #{host}"
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class IO
|
40
|
+
def while_reading(data = nil, &b)
|
41
|
+
while buf = readpartial_rescued(1024)
|
42
|
+
data << buf if data
|
43
|
+
yield buf if block_given?
|
44
|
+
end
|
45
|
+
data
|
46
|
+
end
|
47
|
+
|
48
|
+
def readpartial_rescued(size)
|
49
|
+
readpartial(size)
|
50
|
+
rescue EOFError
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << 'lib'
|
4
|
+
|
5
|
+
require 'client'
|
6
|
+
|
7
|
+
$debug = true
|
8
|
+
|
9
|
+
control_address = tunnel_from_address = tunnel_to_address = remote_listen_address = nil
|
10
|
+
|
11
|
+
(opts = OptionParser.new do |o|
|
12
|
+
o.on("-c", "--control-address ADDRESS") { |a| control_address = a }
|
13
|
+
o.on("-f", "--remote-listen-port ADDRESS") { |a| remote_listen_address = a }
|
14
|
+
o.on("-t", "--tunnel-to ADDRESS") { |a| tunnel_to_address = a }
|
15
|
+
end).parse! rescue (puts opts; exit)
|
16
|
+
|
17
|
+
(puts opts; exit) if [control_address, remote_listen_address, tunnel_to_address].include? nil
|
18
|
+
|
19
|
+
client = RTunnel::Client.new(
|
20
|
+
:control_address => control_address,
|
21
|
+
:remote_listen_address => remote_listen_address,
|
22
|
+
:tunnel_to_address => tunnel_to_address
|
23
|
+
)
|
24
|
+
client.start
|
25
|
+
client.join
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << 'lib'
|
4
|
+
|
5
|
+
require 'server'
|
6
|
+
|
7
|
+
$debug = true
|
8
|
+
|
9
|
+
control_address = tunnel_port = nil
|
10
|
+
|
11
|
+
(opts = OptionParser.new do |o|
|
12
|
+
o.on("-c", "--control ADDRESS") { |a| control_address = a }
|
13
|
+
end).parse! rescue (puts opts; exit)
|
14
|
+
|
15
|
+
server = RTunnel::Server.new(
|
16
|
+
:control_address => control_address
|
17
|
+
)
|
18
|
+
|
19
|
+
server.start
|
20
|
+
server.join
|
data/lib/server.rb
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'gserver'
|
4
|
+
require 'optparse'
|
5
|
+
require 'uuidtools'
|
6
|
+
|
7
|
+
require 'core'
|
8
|
+
require 'cmds'
|
9
|
+
|
10
|
+
Socket.do_not_reverse_lookup = true
|
11
|
+
|
12
|
+
$debug = true
|
13
|
+
|
14
|
+
module RTunnel
|
15
|
+
# listens for incoming connections to tunnel
|
16
|
+
class RemoteListenServer < GServer
|
17
|
+
CONNECTIONS = {}
|
18
|
+
CONTROL_CONNECTION_MAPPING = {}
|
19
|
+
|
20
|
+
def initialize(port, host, control_connection)
|
21
|
+
super(port, host, 10)
|
22
|
+
@control_connection = control_connection
|
23
|
+
@maxConnections = 1024
|
24
|
+
end
|
25
|
+
|
26
|
+
def serve(sock)
|
27
|
+
D "new incoming connection"
|
28
|
+
|
29
|
+
conn_id = UUID.timestamp_create.hexdigest
|
30
|
+
CONNECTIONS[conn_id] = sock
|
31
|
+
CONTROL_CONNECTION_MAPPING[conn_id] = @control_connection
|
32
|
+
begin
|
33
|
+
ControlServer.new_tunnel(conn_id)
|
34
|
+
|
35
|
+
sock.while_reading do |buf|
|
36
|
+
begin
|
37
|
+
ControlServer.send_data(conn_id, buf)
|
38
|
+
rescue Exception
|
39
|
+
D "error talking on control connection, dropping incoming connection: #{$!.inspect}"
|
40
|
+
break
|
41
|
+
end
|
42
|
+
end
|
43
|
+
rescue IOError
|
44
|
+
raise unless $!.message =~ /stream closed/i
|
45
|
+
end
|
46
|
+
|
47
|
+
ControlServer.close_tunnel(conn_id)
|
48
|
+
|
49
|
+
CONNECTIONS.delete conn_id
|
50
|
+
CONTROL_CONNECTION_MAPPING.delete conn_id
|
51
|
+
|
52
|
+
D "sock closed"
|
53
|
+
rescue
|
54
|
+
p $!
|
55
|
+
puts $!.backtrace.join("\n")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class ControlServer < GServer
|
60
|
+
@@control_connections = []
|
61
|
+
@@remote_listen_servers = []
|
62
|
+
|
63
|
+
@@m = Mutex.new
|
64
|
+
|
65
|
+
attr_accessor :ping_interval
|
66
|
+
|
67
|
+
def initialize(*args)
|
68
|
+
super
|
69
|
+
@maxConnections = 1024
|
70
|
+
end
|
71
|
+
|
72
|
+
class << self
|
73
|
+
def new_tunnel(conn_id)
|
74
|
+
D "sending create connection command: #{conn_id}"
|
75
|
+
|
76
|
+
@@m.synchronize { control_connection_for(conn_id).write CreateConnectionCommand.new(conn_id) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def send_data(conn_id, data)
|
80
|
+
@@m.synchronize { control_connection_for(conn_id).write SendDataCommand.new(conn_id, data) }
|
81
|
+
end
|
82
|
+
|
83
|
+
def close_tunnel(conn_id)
|
84
|
+
D "sending close connection command"
|
85
|
+
|
86
|
+
@@m.synchronize { control_connection_for(conn_id).write CloseConnectionCommand.new(conn_id) }
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def control_connection_for(conn_id)
|
92
|
+
RemoteListenServer::CONTROL_CONNECTION_MAPPING[conn_id]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def starting
|
97
|
+
start_pinging
|
98
|
+
end
|
99
|
+
|
100
|
+
def serve(sock)
|
101
|
+
D "new control connection"
|
102
|
+
@@control_connections << sock
|
103
|
+
sock.sync = true
|
104
|
+
|
105
|
+
cmd_queue = ""
|
106
|
+
sock.while_reading(cmd_queue = '') do
|
107
|
+
while Command.match(cmd_queue)
|
108
|
+
case cmd = Command.parse(cmd_queue)
|
109
|
+
when RemoteListenCommand
|
110
|
+
@@m.synchronize do
|
111
|
+
addr, port = cmd.address.split(/:/)
|
112
|
+
if rls = @@remote_listen_servers.detect {|s| s.port == port.to_i }
|
113
|
+
rls.stop
|
114
|
+
@@remote_listen_servers.delete rls
|
115
|
+
end
|
116
|
+
(new_rls = RemoteListenServer.new(port, addr, sock)).start
|
117
|
+
@@remote_listen_servers << new_rls
|
118
|
+
end
|
119
|
+
D "listening for remote connections on #{cmd.address}"
|
120
|
+
when SendDataCommand
|
121
|
+
conn = RemoteListenServer::CONNECTIONS[cmd.conn_id]
|
122
|
+
begin
|
123
|
+
conn.write(cmd.data) if conn
|
124
|
+
rescue Errno::EPIPE
|
125
|
+
D "broken pipe on #{cmd.conn_id}"
|
126
|
+
end
|
127
|
+
when CloseConnectionCommand
|
128
|
+
if connection = RemoteListenServer::CONNECTIONS[cmd.conn_id]
|
129
|
+
D "closing remote connection: #{cmd.conn_id}"
|
130
|
+
connection.close
|
131
|
+
end
|
132
|
+
else
|
133
|
+
D "bad command received: #{cmd.inspect}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
rescue Errno::ECONNRESET
|
138
|
+
D "client disconnected (conn reset)"
|
139
|
+
rescue
|
140
|
+
D $!.inspect
|
141
|
+
D $@*"\n"
|
142
|
+
raise
|
143
|
+
ensure
|
144
|
+
@@control_connections.delete sock
|
145
|
+
end
|
146
|
+
|
147
|
+
def stopping
|
148
|
+
@ping_thread.kill
|
149
|
+
@@remote_listen_server.stop
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def start_pinging
|
155
|
+
@ping_thread = Thread.safe do
|
156
|
+
loop do
|
157
|
+
sleep @ping_interval
|
158
|
+
|
159
|
+
@@m.synchronize do
|
160
|
+
@@control_connections.each {|cc| cc.write PingCommand.new }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class Server
|
168
|
+
def initialize(options = {})
|
169
|
+
@control_address = options[:control_address]
|
170
|
+
if ! @control_address
|
171
|
+
@control_address = "0.0.0.0:#{DEFAULT_CONTROL_PORT}" if ! @control_address
|
172
|
+
elsif @control_address =~ /^\d+$/
|
173
|
+
@control_address.insert 0, "0.0.0.0:"
|
174
|
+
elsif @control_address !~ /:\d+$/
|
175
|
+
@control_address << ":#{DEFAULT_CONTROL_PORT}"
|
176
|
+
end
|
177
|
+
@control_host = @control_address.split(/:/).first
|
178
|
+
|
179
|
+
@ping_interval = options[:ping_interval] || 2.0
|
180
|
+
end
|
181
|
+
|
182
|
+
def start
|
183
|
+
@control_server = ControlServer.new(*@control_address.split(/:/).reverse)
|
184
|
+
@control_server.ping_interval = @ping_interval
|
185
|
+
@control_server.audit = true
|
186
|
+
|
187
|
+
@control_server.start
|
188
|
+
end
|
189
|
+
|
190
|
+
def join
|
191
|
+
@control_server.join
|
192
|
+
end
|
193
|
+
|
194
|
+
def stop
|
195
|
+
@control_server.shutdown
|
196
|
+
ControlServer.stop(@control_server.port, @control_server.host)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/rtunnel.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "rtunnel"
|
3
|
+
s.version = "0.3.8"
|
4
|
+
s.summary = "reverse tunnel server and client"
|
5
|
+
s.email = "coderrr.contact@gmail.com"
|
6
|
+
s.homepage = "http://github.com/coderrr/rtunnel"
|
7
|
+
s.description = "reverse tunnel server and client"
|
8
|
+
s.has_rdoc = false
|
9
|
+
s.authors = ["coderrr"]
|
10
|
+
s.files = [
|
11
|
+
"lib/rtunnel_server_cmd.rb", "lib/client.rb", "lib/core.rb", "lib/rtunnel_client_cmd.rb", "lib/server.rb", "lib/cmds.rb", "bin/rtunnel_server", "bin/rtunnel_client",
|
12
|
+
"ab_test.rb", "README.markdown", "rtunnel.gemspec", "stress_test.rb", "Rakefile", "rtunnel_client.rb", "rtunnel_server.rb"
|
13
|
+
]
|
14
|
+
s.test_files = [
|
15
|
+
"spec/integration_spec.rb", "spec/spec_helper.rb", "spec/client_spec.rb", "spec/cmds_spec.rb", "spec/server_spec.rb"
|
16
|
+
]
|
17
|
+
s.add_dependency("uuidtools", [">= 1.0.2"])
|
18
|
+
end
|
data/rtunnel_client.rb
ADDED
data/rtunnel_server.rb
ADDED
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
require 'client'
|
4
|
+
|
5
|
+
include RTunnel
|
6
|
+
describe RTunnel::Client, "addresses" do
|
7
|
+
def o(opts)
|
8
|
+
{:control_address => 'x.com', :remote_listen_address => '5555', :tunnel_to_address => '6666'}.merge opts
|
9
|
+
end
|
10
|
+
|
11
|
+
before :all do
|
12
|
+
String.class_eval do
|
13
|
+
alias_method :orig_replace_with_ip!, :replace_with_ip!
|
14
|
+
define_method :replace_with_ip! do
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
after :all do
|
21
|
+
String.class_eval do
|
22
|
+
alias_method :replace_with_ip!, :orig_replace_with_ip!
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should use default control port if not specified" do
|
27
|
+
Client.new(o :control_address => 'asdf.net').
|
28
|
+
instance_eval { @control_address }.should == "asdf.net:#{DEFAULT_CONTROL_PORT}"
|
29
|
+
|
30
|
+
Client.new(o :control_address => 'asdf.net:1234').
|
31
|
+
instance_eval { @control_address }.should == "asdf.net:1234"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should use 0.0.0.0 for remote listen host if not specified" do
|
35
|
+
Client.new(o :control_address => 'asdf.net:1234', :remote_listen_address => '8888').
|
36
|
+
instance_eval { @remote_listen_address }.should == '0.0.0.0:8888'
|
37
|
+
|
38
|
+
Client.new(o :control_address => 'asdf.net:1234', :remote_listen_address => 'ip2.asdf.net:8888').
|
39
|
+
instance_eval { @remote_listen_address }.should == 'ip2.asdf.net:8888'
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should use localhost for tunnel to address if not specified" do
|
43
|
+
Client.new(o :tunnel_to_address => '5555').
|
44
|
+
instance_eval { @tunnel_to_address }.should == 'localhost:5555'
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/spec/cmds_spec.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
require 'cmds'
|
4
|
+
|
5
|
+
include RTunnel
|
6
|
+
|
7
|
+
describe RTunnel::CreateConnectionCommand do
|
8
|
+
it "should be able to serialize and parse itself" do
|
9
|
+
serialized = CreateConnectionCommand.new("id").to_s
|
10
|
+
|
11
|
+
cmd = Command.parse(serialized)
|
12
|
+
cmd.class.should == CreateConnectionCommand
|
13
|
+
cmd.conn_id.should == "id"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be able to match itself and remove itself from the stream" do
|
17
|
+
serialized = CreateConnectionCommand.new("abcbdefg").to_s
|
18
|
+
|
19
|
+
Command.match(serialized).should == true
|
20
|
+
Command.parse(serialized)
|
21
|
+
serialized.should be_empty
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe RTunnel::SendDataCommand do
|
26
|
+
it "should be able to serialzied and parse itself" do
|
27
|
+
serialized = SendDataCommand.new("id", "here is some data").to_s
|
28
|
+
|
29
|
+
cmd = Command.parse(serialized)
|
30
|
+
cmd.class.should == SendDataCommand
|
31
|
+
cmd.conn_id.should == "id"
|
32
|
+
cmd.data.should == "here is some data"
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should be able to match itself and remove itself from the stream" do
|
36
|
+
serialized = SendDataCommand.new("abcbdefg", "and here is some data").to_s
|
37
|
+
|
38
|
+
serialized << "WAY MORE CRAP DATA!!!"
|
39
|
+
|
40
|
+
Command.match(serialized).should == true
|
41
|
+
Command.parse(serialized)
|
42
|
+
serialized.should == "WAY MORE CRAP DATA!!!"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe RTunnel::CloseConnectionCommand do
|
47
|
+
it "should be able to serialize and parse itself" do
|
48
|
+
serialized = CloseConnectionCommand.new("id").to_s
|
49
|
+
|
50
|
+
cmd = Command.parse(serialized)
|
51
|
+
cmd.class.should == CloseConnectionCommand
|
52
|
+
cmd.conn_id.should == "id"
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should be able to match itself and remove itself from the stream" do
|
56
|
+
serialized = CloseConnectionCommand.new("abcbdefg").to_s
|
57
|
+
|
58
|
+
Command.match(serialized).should == true
|
59
|
+
Command.parse(serialized)
|
60
|
+
serialized.should be_empty
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe RTunnel::PingCommand do
|
65
|
+
it "should be able to serialize and parse itself" do
|
66
|
+
serialized = PingCommand.new.to_s
|
67
|
+
|
68
|
+
cmd = Command.parse(serialized)
|
69
|
+
cmd.class.should == PingCommand
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should be able to match itself and remove itself from the stream" do
|
73
|
+
serialized = PingCommand.new.to_s
|
74
|
+
|
75
|
+
Command.match(serialized).should == true
|
76
|
+
Command.parse(serialized)
|
77
|
+
serialized.should be_empty
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
describe RTunnel::RemoteListenCommand do
|
83
|
+
it "should be able to serialize and parse itself" do
|
84
|
+
serialized = RemoteListenCommand.new("0.0.0.0:1234").to_s
|
85
|
+
|
86
|
+
cmd = Command.parse(serialized)
|
87
|
+
cmd.class.should == RemoteListenCommand
|
88
|
+
cmd.address.should == "0.0.0.0:1234"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should be able to match itself and remove itself from the stream" do
|
92
|
+
serialized = RemoteListenCommand.new("0.0.0.0:1234").to_s
|
93
|
+
|
94
|
+
Command.match(serialized).should == true
|
95
|
+
Command.parse(serialized)
|
96
|
+
serialized.should be_empty
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
describe RTunnel::Command do
|
102
|
+
it "should be able to match a command in a stream" do
|
103
|
+
Command.match("C1234abc|").should == true
|
104
|
+
Command.match("C1234abc").should == false
|
105
|
+
Command.match("C1234abc|C|").should == true
|
106
|
+
|
107
|
+
Command.match("Did|15|DATA").should == false
|
108
|
+
Command.match("Did|1|DATA").should == true
|
109
|
+
Command.match("Did|4|\0\0\0\0").should == true
|
110
|
+
|
111
|
+
data = "C1234abc|D1234abc|15|here is my dataX1234abc|L0.0.0.0:1234|CASDF"
|
112
|
+
cmd1 = Command.parse(data)
|
113
|
+
cmd2 = Command.parse(data)
|
114
|
+
cmd3 = Command.parse(data)
|
115
|
+
cmd4 = Command.parse(data)
|
116
|
+
cmd1.class.should == CreateConnectionCommand
|
117
|
+
cmd1.conn_id.should == "1234abc"
|
118
|
+
cmd2.class.should == SendDataCommand
|
119
|
+
cmd2.conn_id.should == "1234abc"
|
120
|
+
cmd3.class.should == CloseConnectionCommand
|
121
|
+
cmd3.conn_id.should == "1234abc"
|
122
|
+
cmd4.class.should == RemoteListenCommand
|
123
|
+
cmd4.address.should == "0.0.0.0:1234"
|
124
|
+
|
125
|
+
Command.match(data).should == false
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
require 'client'
|
4
|
+
require 'server'
|
5
|
+
|
6
|
+
describe "RTunnel" do
|
7
|
+
class TextServer < GServer
|
8
|
+
def initialize(port, text)
|
9
|
+
@text = text
|
10
|
+
super(port)
|
11
|
+
end
|
12
|
+
|
13
|
+
def serve(io)
|
14
|
+
io.write(@text)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def read_from_address(addr)
|
19
|
+
timeout(2) { TCPSocket.open(*addr.split(/:/)) {|s| s.read } }
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should work!" do
|
23
|
+
begin
|
24
|
+
server = RTunnel::Server.new(:control_address => 'localhost:9999')
|
25
|
+
client = RTunnel::Client.new(:control_address => 'localhost:9999', :remote_listen_address => '30002', :tunnel_to_address => '30003')
|
26
|
+
|
27
|
+
server.start
|
28
|
+
client.start
|
29
|
+
|
30
|
+
s = TextServer.new(30003, echo_text = "tunnel this txt plz!")
|
31
|
+
s.start
|
32
|
+
|
33
|
+
sleep 0.5
|
34
|
+
|
35
|
+
# direct connect (sanity check)
|
36
|
+
read_from_address('localhost:30003').should == echo_text
|
37
|
+
# tunneled connect
|
38
|
+
read_from_address('localhost:30002').should == echo_text
|
39
|
+
ensure
|
40
|
+
begin
|
41
|
+
client.stop
|
42
|
+
server.stop
|
43
|
+
s.stop
|
44
|
+
rescue
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should ping all clients periodically" do
|
50
|
+
begin
|
51
|
+
server = RTunnel::Server.new(:control_address => 'localhost:9999', :ping_interval => 0.2)
|
52
|
+
clients = []
|
53
|
+
clients << RTunnel::Client.new(:control_address => 'localhost:9999', :remote_listen_address => '30002', :tunnel_to_address => '30003')
|
54
|
+
clients << RTunnel::Client.new(:control_address => 'localhost:9999', :remote_listen_address => '30012', :tunnel_to_address => '30013')
|
55
|
+
server.start
|
56
|
+
clients.each{|c|c.start}
|
57
|
+
|
58
|
+
pings = Hash.new {|h,k| h[k] = [] }
|
59
|
+
t = Thread.new do
|
60
|
+
loop do
|
61
|
+
clients.each do |client|
|
62
|
+
pings[client] << client.instance_eval { @last_ping }
|
63
|
+
end
|
64
|
+
sleep 0.05
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
sleep 2
|
69
|
+
|
70
|
+
clients.each do |client|
|
71
|
+
# 2 seconds, 0.2 pings a sec = ~10 pings
|
72
|
+
(9..11).should include(pings[client].uniq.size)
|
73
|
+
end
|
74
|
+
ensure
|
75
|
+
t.kill
|
76
|
+
begin
|
77
|
+
clients.each{|c|c.stop}
|
78
|
+
server.stop
|
79
|
+
rescue
|
80
|
+
p $!,$@
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it "the client shouldnt fail even if there is no server running at the tunnel_to address" do
|
86
|
+
begin
|
87
|
+
server = RTunnel::Server.new(:control_address => 'localhost:9999')
|
88
|
+
client = RTunnel::Client.new(:control_address => 'localhost:9999', :remote_listen_address => '30002', :tunnel_to_address => '30003')
|
89
|
+
|
90
|
+
server.start
|
91
|
+
client.start
|
92
|
+
|
93
|
+
sleep 0.5
|
94
|
+
|
95
|
+
# tunneled connect
|
96
|
+
read_from_address('localhost:30002').should be_empty
|
97
|
+
ensure
|
98
|
+
begin
|
99
|
+
client.stop
|
100
|
+
server.stop
|
101
|
+
rescue
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/spec/server_spec.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
require 'server'
|
4
|
+
|
5
|
+
include RTunnel
|
6
|
+
describe RTunnel::Server, "addresses" do
|
7
|
+
it "control address should listen on all interfaces and use default control port if not specified" do
|
8
|
+
Server.new(:control_address => nil).
|
9
|
+
instance_eval { @control_address }.should == "0.0.0.0:#{DEFAULT_CONTROL_PORT}"
|
10
|
+
|
11
|
+
Server.new(:control_address => '5555').
|
12
|
+
instance_eval { @control_address }.should == "0.0.0.0:5555"
|
13
|
+
|
14
|
+
Server.new(:control_address => 'interface2').
|
15
|
+
instance_eval { @control_address }.should == "interface2:#{DEFAULT_CONTROL_PORT}"
|
16
|
+
|
17
|
+
Server.new(:control_address => 'interface2:2222').
|
18
|
+
instance_eval { @control_address }.should == "interface2:2222"
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/stress_test.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'mongrel'
|
4
|
+
require 'facets'
|
5
|
+
require 'facets/random'
|
6
|
+
require 'logger'
|
7
|
+
require 'stringio'
|
8
|
+
|
9
|
+
require 'lib/core'
|
10
|
+
|
11
|
+
pids = []
|
12
|
+
at_exit do
|
13
|
+
pids.each {|pid| Process.kill 9, pid }
|
14
|
+
p $!
|
15
|
+
puts "done, hit ^C"
|
16
|
+
sleep 999999
|
17
|
+
end
|
18
|
+
|
19
|
+
module Enumerable
|
20
|
+
def parallel_map
|
21
|
+
self.map do |e|
|
22
|
+
Thread.new(e) do |element|
|
23
|
+
yield element
|
24
|
+
end
|
25
|
+
end.map {|thread| thread.value }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
CONCURRENT_CONNECTIONS = 25
|
30
|
+
|
31
|
+
TUNNEL_PORT = 5000
|
32
|
+
HTTP_PORT = 4444
|
33
|
+
TUNNEL_URI = "http://localhost:#{TUNNEL_PORT}"
|
34
|
+
EXPECTED_DATA = String.random(10*1024)
|
35
|
+
|
36
|
+
puts EXPECTED_DATA.inspect
|
37
|
+
|
38
|
+
p :gend_random_data
|
39
|
+
|
40
|
+
require 'thin'
|
41
|
+
app = lambda { |env| [200, {}, EXPECTED_DATA] }
|
42
|
+
server = ::Thin::Server.new('localhost', HTTP_PORT, app)
|
43
|
+
Thread.new { server.start }
|
44
|
+
|
45
|
+
p :started_stressed_server
|
46
|
+
|
47
|
+
base_dir = File.dirname(__FILE__)
|
48
|
+
|
49
|
+
pids << fork{ exec "ruby #{base_dir}/rtunnel_server.rb > /dev/null 2>&1" }
|
50
|
+
pids << fork{ exec "ruby #{base_dir}/rtunnel_client.rb -c localhost -f #{TUNNEL_PORT} -t #{HTTP_PORT} > /dev/null 2>&1" }
|
51
|
+
|
52
|
+
p :started_rtunnels
|
53
|
+
|
54
|
+
sleep 2
|
55
|
+
|
56
|
+
p :slept
|
57
|
+
|
58
|
+
STDOUT.sync = true
|
59
|
+
999999999.times do |i|
|
60
|
+
puts i if i%10 == 0
|
61
|
+
threads = []
|
62
|
+
CONCURRENT_CONNECTIONS.times do
|
63
|
+
threads << Thread.new do
|
64
|
+
text = %x{curl --silent #{TUNNEL_URI}}
|
65
|
+
if text != EXPECTED_DATA
|
66
|
+
puts "BAD!!!!"*1000
|
67
|
+
puts "response: #{text.inspect}"
|
68
|
+
exit
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
threads.parallel_map {|t| t.join; print '.'; STDOUT.flush }
|
74
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: coderrr-rtunnel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.8
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- coderrr
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-10-25 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: uuidtools
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.0.2
|
23
|
+
version:
|
24
|
+
description: reverse tunnel server and client
|
25
|
+
email: coderrr.contact@gmail.com
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files: []
|
31
|
+
|
32
|
+
files:
|
33
|
+
- lib/rtunnel_server_cmd.rb
|
34
|
+
- lib/client.rb
|
35
|
+
- lib/core.rb
|
36
|
+
- lib/rtunnel_client_cmd.rb
|
37
|
+
- lib/server.rb
|
38
|
+
- lib/cmds.rb
|
39
|
+
- bin/rtunnel_server
|
40
|
+
- bin/rtunnel_client
|
41
|
+
- ab_test.rb
|
42
|
+
- README.markdown
|
43
|
+
- rtunnel.gemspec
|
44
|
+
- stress_test.rb
|
45
|
+
- Rakefile
|
46
|
+
- rtunnel_client.rb
|
47
|
+
- rtunnel_server.rb
|
48
|
+
has_rdoc: false
|
49
|
+
homepage: http://github.com/coderrr/rtunnel
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: "0"
|
66
|
+
version:
|
67
|
+
requirements: []
|
68
|
+
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 1.2.0
|
71
|
+
signing_key:
|
72
|
+
specification_version: 2
|
73
|
+
summary: reverse tunnel server and client
|
74
|
+
test_files:
|
75
|
+
- spec/integration_spec.rb
|
76
|
+
- spec/spec_helper.rb
|
77
|
+
- spec/client_spec.rb
|
78
|
+
- spec/cmds_spec.rb
|
79
|
+
- spec/server_spec.rb
|