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)
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
26 # Gateways are servers listening for master and client connections,
27 # searching to provide a tunnel between them.
29 class Gateway < Daemon
31 attr_reader :host, :port, :master
34 # Create a gateway accepting connections at +host+:+port+, using
35 # +master+ as the master's certificate subject.
37 def initialize(host, port, master)
40 @host = host || "0.0.0.0"
47 @connmap.extend MonitorMixin
48 @connmap_cond = @connmap.new_cond
50 @connid_lock = Mutex.new
56 # Wait for masters and clients and handle their connections.
60 tcps = TCPServer.new @host, @port
62 @logger.fatal("Unable to listen on #{@host}:#{@port}.")
65 @logger.info("Waiting for master on #{@host}:#{@port}.")
69 rawclient = tcps.accept
71 @logger.info("Terminating gracefully.")
74 @logger.warn("Error while accepting connection: #{e}.")
78 register_thread(rawclient) do
80 client = OpenSSL::SSL::SSLSocket.new(rawclient, context)
81 client.sync_close = true
83 rescue SSL::SSLError => e
84 @logger.warn("SSL Error while accepting connection: #{e}.")
89 peerhost = client.peeraddr[3]
90 peerport = client.peeraddr[1]
92 if client.peer_cert.subject.to_s == @master
93 @logger.info("Master connected from " +
94 "#{peerhost}:#{peerport}.")
98 @logger.warn("Client #{client.peer_cert.subject} " +
99 "(#{peerhost}:#{peerport}) ignored because " +
100 "master is offline.")
101 client.puts "sorry, master offline"
104 @logger.info("Client #{client.peer_cert.subject} " +
105 "connected from #{peerhost}:#{peerport}.")
120 @logger.info("Gateway terminated.")
125 # Talk to the master.
127 def do_master(socket)
128 socket.puts "gateway connection ready"
133 socket.puts "already online"
136 @logger.info "Master has opened control connection."
140 @logger.info "Control connection closed."
142 when /^this is connection (\d+)$/
144 @logger.info "Master provides connection #{id}."
145 thread = @connmap[id]
146 @connmap.synchronize do
147 @connmap[id] = socket
153 @logger.info "Master cancelled connection."
159 # Perform control (master - gateway) connection on +socket+.
161 def do_control(socket)
163 ios, dummy = select [@connpipe[0], socket]
165 if ios.include? socket
169 when /^(\d+): connection failed$/
170 @connmap.synchronize do
171 @connmap[$1.to_i] = :error
174 when /^(\d+): in progress$/
175 @connmap.synchronize do
176 @connmap[$1.to_i] = :success
180 @connmap.synchronize do
181 @connmap[$1.to_i] = :error
187 if ios.include? @connpipe[0]
188 socket.write @connpipe[0].gets
194 # Handle a client connection request.
196 def do_client(socket)
197 peerhost = socket.peeraddr[3]
198 peerport = socket.peeraddr[1]
199 socket.puts "gateway, how can I help?"
201 case line = socket.gets
202 when /^connect me to port (\d+) on (.*)$/
204 # Get a connection id
205 @connid_lock.synchronize do
208 @logger.info("Connection #{id}: " +
209 "#{peerhost}:#{peerport} <=> " +
212 # Wait for master to open connection
213 @connmap.synchronize do
214 @connmap[id] = Thread.current
215 @connpipe[1].puts "#{id}: #{line.rstrip}"
216 @connmap_cond.wait_while { @connmap[id] == Thread.current }
218 master = @connmap.delete(id)
220 if master == :error or master == :success
221 # Master failed to connect (e.g. no such host)
224 # Client may start talking
225 socket.puts "have fun"
227 ios, dummy = select [socket, master]
230 when socket then master
234 data = io.sysread(BLOCKSIZE)
242 break if master.closed? or socket.closed?
246 when /^directly connect me \(([^:]+):(\d+)\) to port (\d+) on (.*)$/
248 @connid_lock.synchronize do
251 @logger.info("Connection #{id}: direct, " +
252 "#{$1}:#{$2} <=> #{$4}:#{$3}")
254 # Wait for master to be ready
255 @connmap.synchronize do
256 @connmap[id] = Thread.current
257 @connpipe[1].puts("#{id}: directly connect #{$1}:#{$2} " +
258 "to port #{$3} on #{$4}; DN is " +
259 socket.peer_cert.subject)
260 @connmap_cond.wait_while { @connmap[id] == Thread.current }
262 success = @connmap.delete(id)
264 if success == :success
265 socket.puts "in progress"
267 socket.puts "master failed"
276 @logger.info("Client #{peerhost}:#{peerport} disconnected.")