costan-rtunnel 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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