Implemented keep-alive packets.
[clw.git] / lib / clw / master.rb
blob1cc090a9a34ff64432614c18b2ea41e285bb8514
1 # Clarkway tunnelling service
2 # Copyright (C) 2007 Michael Schutte
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License as published by the Free
6 # Software Foundation; either version 2 of the License, or (at your option)
7 # any later version.
9 # This program is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
12 # more details.
14 # Modules
15 require "clw/daemon"
17 # External modules
18 require "thread"
19 require "socket"
20 require "openssl"
22 module Clarkway
24     #
25     # A master connects to a gateway and allows it to open arbitrary
26     # connections.
27     #
28     class Master < Daemon
30         attr_reader :host, :port, :gateway, :reconnect
32         #
33         # Create a master connecting to +host+:+port+, expecting the peer
34         # certificate with subject +gateway+, retrying every +reconnect+
35         # seconds.
36         #
37         def initialize(host, port, gateway, reconnect)
38             super()
40             @host = host
41             @port = port
42             @gateway = gateway
43             @reconnect = reconnect
44         end
46         protected
48         #
49         # Loop forever, trying to establish a gateway connection.  When
50         # connected to a gateway, start regular work.
51         #
52         def main
53             loop do
54                 begin
55                     tcps = TCPSocket.new @host, @port
56                     ssls = SSL::SSLSocket.new tcps, context
57                     ssls.connect
58                     ssls.sync_close = true
59                 rescue
60                     @logger.warn("Gateway not available.")
61                 else
62                     if ssls.peer_cert.subject.to_s != @gateway
63                         @logger.warn("Possible spoofing attempt! Gateway " +
64                                      "identified as #{ssls.peer_cert.subject}, " +
65                                      "not starting operation.")
66                     else
67                         @logger.info("Gateway connection established.")
68                         begin
69                             do_gateway ssls
70                         rescue Interrupt
71                             @logger.info("Terminating gracefully.")
72                             break
73                         end
74                     end
75                 ensure
76                     ssls.close unless ssls.nil?
77                 end
78                 @logger.info("Retrying in #{@reconnect} seconds.")
79                 begin
80                     sleep @reconnect
81                 rescue Interrupt
82                     @logger.info "Terminating gracefully."
83                     break
84                 end
85             end
87             stop
88         end
90         #
91         # Stop master operations.
92         #
93         def stop
94             @logger.info("Master terminated.")
95             super
96         end
98         #
99         # Perform control (master - gateway) connection on +ctrl+.
100         #
101         def do_gateway(ctrl)
102             begin
103                 if ctrl.gets !~ /^gateway connection ready$/
104                     @logger.warn "Gateway protocol mismatch."
105                     ctrl.puts "sorry"
106                     return
107                 end
109                 ctrl.puts "master, how can I help?"
111                 while line = ctrl.gets
112                     case line
113                     when /^thanks$/
114                         ctrl.puts "always happy to serve"
115                         break
116                     when /^already online$/
117                         break
118                     when /^keepalive$/
119                         ctrl.puts "okay"
120                     when /^(\d+): connect me to port (\d+) on (.*)$/
121                         id = $1.to_i
122                         host = $3
123                         port = $2.to_i
124                         register_thread(id) do
125                             do_connect ctrl, id, host, port
126                         end
127                     when /^(\d+):\ directly\ connect\ ([^:]*):(\d+)
128                             \ to\ port\ (\d+)\ on\ ([^;]*);\ DN\ is\ (.*)$/x
129                         id = $1
130                         chost = $2
131                         cport = $3.to_i
132                         host = $5
133                         port = $4.to_i
134                         dn = $6
135                         register_thread(id) do
136                             do_direct ctrl, id, chost, cport, dn, host, port
137                         end
138                     when /^(\d+):/
139                         ctrl.puts "#{$1}: sorry"
140                     else
141                         ctrl.puts "sorry"
142                     end
143                 end
144             rescue
145                 @logger.warn("Gateway connection lost.")
146             else
147                 @logger.warn("Gateway connection closed.")
148             end
149         end
151         #
152         # The select loop connecting Clarkway client and a TCP socket.
153         #
154         def select_loop(client, socket)
155             loop do
156                 ios, dummy = select [client, socket]
157                 ios.each do |io|
158                     target = case io
159                              when socket then   client
160                              else               socket
161                              end
162                     begin
163                         data = io.sysread(BLOCKSIZE)
164                     rescue EOFError
165                         io.close
166                         target.close
167                         break
168                     end
169                     target.write data
170                 end
171                 break if client.closed? or socket.closed?
172             end
173         end
175         #
176         # Handle a normal connection request.
177         #
178         def do_connect(ctrl, id, host, port)
179             @logger.info("Gateway requested connection to #{host}:#{port}.")
181             # Connect to the requested TCP server
182             begin
183                 socket = TCPSocket.new host, port
184             rescue
185                 ctrl.puts "#{id}: connection failed"
186                 return
187             end
189             # Connect back to the gateway
190             begin
191                 gateway = TCPSocket.new @host, @port
192                 gateway = SSL::SSLSocket.new gateway, context
193                 gateway.connect
194                 gateway.sync_close = true
196                 if gateway.peer_cert.subject.to_s != @gateway
197                     @logger.warn("Possible spoofing attempt! While connecting " +
198                                  "back, gateway identified as " +
199                                  "#{gateway.peer_cert.subject}.")
200                     return
201                 end
203                 gateway.gets
204                 gateway.puts "this is connection #{id}"
205                 @logger.info("Gateway connected to #{host}:#{port}.")
206                 select_loop gateway, socket
207             rescue
208                 ctrl.puts "#{id}: connection failed"
209                 @logger.warn("I/O error between gateway and #{host}:#{port}.")
210             ensure
211                 socket.close unless socket.closed?
212                 gateway.close unless gateway.closed?
213                 @logger.info("Closed connection between gateway and " +
214                              "#{host}:#{port}.")
215             end
216         end
218         #
219         # Directly connect back to the client.
220         #
221         def do_direct(ctrl, id, chost, cport, dn, host, port)
222             @logger.info("#{chost}:#{cport} requested connection to #{host}:#{port}.")
224             # Connect to the Clarkway client
225             begin
226                 tcpclient = TCPSocket.new chost, cport
227                 client = SSL::SSLSocket.new tcpclient, context
228                 client.connect
229                 client.sync_close = true
230             rescue => e
231                 @logger.warn("Error on direct connection: #{e}")
232                 ctrl.puts "#{id}: unable to connect"
233                 return
234             end
236             if client.peer_cert.subject.to_s != dn
237                 client.close
238                 @logger.warn("Possible spoofing attempt! Wrong client ",
239                              "identification on direct connection.")
240                 ctrl.puts "#{id}: unable to connect"
241                 return
242             end
244             ctrl.puts "#{id}: in progress"
246             # Connect to the requested TCP server
247             begin
248                 socket = TCPSocket.new host, port
249             rescue => e
250                 client.puts "connection failed"
251                 client.close
252                 return
253             end
255             @logger.info("Client #{client.peer_cert.subject} " + 
256                          "from #{chost}:#{cport} connected to " +
257                          "#{host}:#{port}.")
258             begin
259                 client.puts "have fun"
260                 select_loop client, socket
261             rescue
262                 @logger.warn("I/O error between #{chost}:#{cport} " +
263                              "and #{host}:#{port}.")
264             ensure
265                 socket.close unless socket.closed?
266                 client.close unless client.closed?
267                 @logger.info("Closed connection to #{chost}:#{cport}.")
268             end
269         end
271     end