costan-rtunnel 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/CHANGELOG +13 -0
  2. data/LICENSE +21 -0
  3. data/Manifest +49 -0
  4. data/README.markdown +84 -0
  5. data/Rakefile +45 -0
  6. data/bin/rtunnel_client +4 -0
  7. data/bin/rtunnel_server +4 -0
  8. data/lib/rtunnel.rb +20 -0
  9. data/lib/rtunnel/client.rb +308 -0
  10. data/lib/rtunnel/command_processor.rb +62 -0
  11. data/lib/rtunnel/command_protocol.rb +50 -0
  12. data/lib/rtunnel/commands.rb +233 -0
  13. data/lib/rtunnel/connection_id.rb +24 -0
  14. data/lib/rtunnel/core.rb +58 -0
  15. data/lib/rtunnel/crypto.rb +106 -0
  16. data/lib/rtunnel/frame_protocol.rb +34 -0
  17. data/lib/rtunnel/io_extensions.rb +54 -0
  18. data/lib/rtunnel/leak.rb +35 -0
  19. data/lib/rtunnel/rtunnel_client_cmd.rb +41 -0
  20. data/lib/rtunnel/rtunnel_server_cmd.rb +32 -0
  21. data/lib/rtunnel/server.rb +351 -0
  22. data/lib/rtunnel/socket_factory.rb +119 -0
  23. data/spec/client_spec.rb +47 -0
  24. data/spec/cmds_spec.rb +127 -0
  25. data/spec/integration_spec.rb +105 -0
  26. data/spec/server_spec.rb +21 -0
  27. data/spec/spec_helper.rb +3 -0
  28. data/test/command_stubs.rb +77 -0
  29. data/test/protocol_mocks.rb +43 -0
  30. data/test/scenario_connection.rb +109 -0
  31. data/test/test_client.rb +48 -0
  32. data/test/test_command_protocol.rb +82 -0
  33. data/test/test_commands.rb +49 -0
  34. data/test/test_connection_id.rb +30 -0
  35. data/test/test_crypto.rb +127 -0
  36. data/test/test_frame_protocol.rb +109 -0
  37. data/test/test_io_extensions.rb +70 -0
  38. data/test/test_server.rb +70 -0
  39. data/test/test_socket_factory.rb +42 -0
  40. data/test/test_tunnel.rb +186 -0
  41. data/test_data/authorized_keys2 +4 -0
  42. data/test_data/known_hosts +4 -0
  43. data/test_data/random_rsa_key +27 -0
  44. data/test_data/ssh_host_dsa_key +12 -0
  45. data/test_data/ssh_host_rsa_key +27 -0
  46. data/tests/_ab_test.rb +16 -0
  47. data/tests/_stress_test.rb +96 -0
  48. data/tests/lo_http_server.rb +55 -0
  49. metadata +121 -0
@@ -0,0 +1,119 @@
1
+ require 'socket'
2
+
3
+ module RTunnel::SocketFactory
4
+ def self.split_address(address)
5
+ port_index = address.index /[^:]\:[^:]/
6
+ if port_index
7
+ [address[0, port_index + 1], address[port_index + 2, address.length]]
8
+ else
9
+ [address, nil]
10
+ end
11
+ end
12
+
13
+ def self.host_from_address(address)
14
+ address and split_address(address)[0]
15
+ end
16
+
17
+ def self.port_from_address(address)
18
+ address and (port_string = split_address(address)[1]) and port_string.to_i
19
+ end
20
+
21
+ def self.inbound?(options)
22
+ options[:inbound] or [:in_port, :in_host, :in_addr].any? { |k| options[k] }
23
+ end
24
+
25
+ def self.bind_host(options)
26
+ options[:in_host] or host_from_address(options[:in_addr]) or '0.0.0.0'
27
+ end
28
+
29
+ def self.bind_port(options)
30
+ options[:in_port] or port_from_address(options[:in_addr]) or 0
31
+ end
32
+
33
+ def self.bind_socket_address(options)
34
+ Socket::pack_sockaddr_in bind_port(options), bind_host(options)
35
+ end
36
+
37
+ def self.connect_host(options)
38
+ options[:out_host] or host_from_address(options[:out_addr])
39
+ end
40
+
41
+ def self.connect_port(options)
42
+ options[:out_port] or port_from_address(options[:out_addr])
43
+ end
44
+
45
+ def self.connect_socket_address(options)
46
+ Socket::pack_sockaddr_in connect_port(options), connect_host(options)
47
+ end
48
+
49
+ def self.tcp?(options)
50
+ options[:tcp] or !options[:udp]
51
+ end
52
+
53
+ def self.new_tcp_socket
54
+ Socket.new Socket::AF_INET, Socket::SOCK_STREAM, Socket::PF_UNSPEC
55
+ end
56
+
57
+ def self.new_udp_socket
58
+ Socket.new Socket::AF_INET, Socket::SOCK_DGRAM, Socket::PF_UNSPEC
59
+ end
60
+
61
+ def self.new_socket(options)
62
+ tcp?(options) ? new_tcp_socket : new_udp_socket
63
+ end
64
+
65
+ def self.bind(socket, options)
66
+ socket.bind bind_socket_address(options)
67
+ end
68
+
69
+ def self.connect(socket, options)
70
+ socket.connect connect_socket_address(options)
71
+ end
72
+
73
+ def self.set_options(socket, options)
74
+ if options[:no_delay]
75
+ socket.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true
76
+ socket.sync = true
77
+ end
78
+
79
+ if options[:reuse_addr]
80
+ socket.setsockopt Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true
81
+ end
82
+
83
+ unless options[:reverse_lookup]
84
+ if socket.respond_to? :do_not_reverse_lookup
85
+ socket.do_not_reverse_lookup = true
86
+ else
87
+ # work around until the patch below actually gets committed:
88
+ # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/2346
89
+ BasicSocket.do_not_reverse_lookup = true
90
+ end
91
+ end
92
+ end
93
+
94
+ # new sockets coming out of socket.accept will have the given options set
95
+ def self.set_options_on_accept_sockets(socket, options)
96
+ socket.instance_variable_set :@rtunnel_factory_options, options
97
+ def socket.accept(*args)
98
+ sock, addr = super
99
+ RTunnel::SocketFactory.set_options sock, @rtunnel_factory_options
100
+ return sock, addr
101
+ end
102
+ end
103
+
104
+ def self.socket(options = {})
105
+ s = new_socket options
106
+ set_options s, options
107
+ if inbound? options
108
+ bind s, options
109
+ set_options_on_accept_sockets s, options
110
+ else
111
+ connect s, options
112
+ end
113
+ s
114
+ end
115
+
116
+ def socket(options = {})
117
+ RTunnel::SocketFactory.socket(options)
118
+ end
119
+ end
@@ -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'
@@ -0,0 +1,77 @@
1
+ # Contains generate_ and verify_ names for all RTunnel commands.
2
+ module CommandStubs
3
+ @@test_id1 = "1029384756ALSKDJFH"
4
+ @@test_id2 = "ALSKDJFH1029384756"
5
+ @@test_address = "192.168.43.95"
6
+ @@data = (0..255).to_a.pack('C*')
7
+ @@ekey = (128...192).to_a.pack('C*') * 8
8
+ @@pubfp = (0...128).to_a.pack('C*') * 5
9
+
10
+ def generate_keep_alive
11
+ RTunnel::KeepAliveCommand.new
12
+ end
13
+ def verify_keep_alive(cmd)
14
+ assert_equal RTunnel::KeepAliveCommand, cmd.class
15
+ end
16
+
17
+ def generate_create
18
+ RTunnel::CreateConnectionCommand.new @@test_id1
19
+ end
20
+ def verify_create(cmd)
21
+ assert_equal RTunnel::CreateConnectionCommand, cmd.class
22
+ assert_equal @@test_id1, cmd.connection_id
23
+ end
24
+
25
+ def generate_close
26
+ RTunnel::CloseConnectionCommand.new @@test_id2
27
+ end
28
+ def verify_close(cmd)
29
+ assert_equal RTunnel::CloseConnectionCommand, cmd.class
30
+ assert_equal @@test_id2, cmd.connection_id
31
+ end
32
+
33
+ def generate_listen
34
+ RTunnel::RemoteListenCommand.new @@test_address
35
+ end
36
+ def verify_listen(cmd)
37
+ assert_equal RTunnel::RemoteListenCommand, cmd.class
38
+ assert_equal @@test_address, cmd.address
39
+ end
40
+
41
+ def generate_send
42
+ RTunnel::SendDataCommand.new @@test_id1, @@data
43
+ end
44
+ def verify_send(cmd)
45
+ assert_equal RTunnel::SendDataCommand, cmd.class
46
+ assert_equal @@test_id1, cmd.connection_id
47
+ assert_equal @@data, cmd.data
48
+ end
49
+
50
+ def generate_set_sk
51
+ RTunnel::SetSessionKeyCommand.new @@ekey
52
+ end
53
+ def verify_set_sk(cmd)
54
+ assert_equal RTunnel::SetSessionKeyCommand, cmd.class
55
+ assert_equal @@ekey, cmd.encrypted_keys
56
+ end
57
+
58
+ def generate_gen_sk
59
+ RTunnel::GenerateSessionKeyCommand.new @@pubfp
60
+ end
61
+ def verify_gen_sk(cmd)
62
+ assert_equal RTunnel::GenerateSessionKeyCommand, cmd.class
63
+ assert_equal @@pubfp, cmd.public_key_fp
64
+ end
65
+
66
+ # An array with the names of all commands.
67
+ # Use these names to obtain the names of the genrate_ and verify_ methods.
68
+ def self.command_names
69
+ [:keep_alive, :create, :close, :listen, :send, :gen_sk, :set_sk]
70
+ end
71
+
72
+ # A sequence of command names useful for testing "real" connections.
73
+ def self.command_test_sequence
74
+ [:create, :keep_alive, :gen_sk, :set_sk, :keep_alive, :listen, :keep_alive,
75
+ :send, :send, :keep_alive, :keep_alive, :send, :close]
76
+ end
77
+ end