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 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/ruby
2
+
3
+ load 'rtunnel_client_cmd.rb'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/ruby
2
+
3
+ load 'rtunnel_server_cmd.rb'
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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/ruby
2
+
3
+ load 'lib/rtunnel_client_cmd.rb'
data/rtunnel_server.rb ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/ruby
2
+
3
+ load 'lib/rtunnel_server_cmd.rb'
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH << File.dirname(__FILE__) + "/../lib"
2
+
3
+ require 'spec'
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