2 # Clarkway tunnelling service
3 # Copyright (C) 2007 Michael Schutte
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the Free
7 # Software Foundation; either version 2 of the License, or (at your option)
10 # This program is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
25 # Print command line usage information.
29 Usage: clw MODE|OPTION ...
30 This is #{Clarkway::ID}, an SSL tunnelling application.
33 master Connect to a gateway and obey to its connection
35 gateway Accept connections from clients and the master and
36 moderate between them.
37 client Connect to a gateway and get forwarded to the
39 PROFILE User defined profiles are stored as YAML files in
40 ~/.clw/profiles. See clw(1) for details.
41 FILE Loads user defined profile from FILE. This only
42 works if the file name contains dots or slashes; if
43 it does not, use something like ./FILE.
46 --ca FILE Use FILE as certificate authority; mandatory.
47 --cert FILE Use FILE as certificate; mandatory.
48 --key FILE Use FILE as private key; mandatory.
49 --crl FILE Use FILE as certificate revocation list; optional.
50 --subject SUBJECT Expect peer to identify with the given certificate
52 --gateway HOST[:PORT] Host and port to connect to; mandatory for master
53 and client, ignored for gateway operation.
54 --reconnect N Master: Try to reconnect to the gateway after N
56 --keepalive N Gateway: Send keep-alive lines to avoid mysteriously
57 closing connections every N seconds [0]; a value of
58 zero disables the feature.
59 --connect HOST:PORT Host and port to connect to; mandatory for client,
61 --[no-]stdio Client uses standard input and output for the
62 communication with the target.
63 --listen [HOST:]PORT Gateway: Listen on host HOST [0.0.0.0], port PORT
65 Client: Listen on HOST [127.0.0.1], port PORT
68 --direct HOST[:PORT] Client: Instead of redirecting traffic through the
69 gateway, listen on PORT and tell the master to
70 connect to HOST:PORT. HOST must be this host's
72 --[no-]once Client closes server after first connection.
73 --[no-]daemon (Do not) daemonize after startup. Daemon mode is
74 enabled by default, except for clients acting in
76 --exec COMMAND Client runs COMMAND after opening server (if
77 --listen is active) or talks to it using a pipe (in
79 --pidfile FILE On startup, write process ID to FILE. If FILE
80 exists and creates a PID of a running process,
82 --logfile FILE Write logs to FILE. Default is to disable logging if
83 daemon mode is enabled, standard error output
84 otherwise. The special value `-' logs to stderr,
85 `off' forces to disable logging.
86 --loglevel LEVEL Minimum level (info, warn, fatal) of log messages.
87 Default is to log everything (info).
88 --logname NAME Log as program NAME. Useful if multiple instances
89 of Clarkway log into the same file.
90 --help Show this help.
92 Please report bugs to Michael Schutte <m.schutte.jr@gmail.com>.
99 class CommandLineParser
101 class Error
< RuntimeError
; end
103 attr_reader
:args, :options
105 CLWHOME
= ENV["HOME"] + "/.clw"
113 "subject" => :string,
114 "gateway" => :string,
115 "reconnect" => :integer,
116 "keepalive" => :integer,
117 "connect" => :string,
122 "daemon" => :boolean,
124 "pidfile" => :string,
125 "logfile" => :string,
126 "loglevel" => :string,
127 "logname" => :string,
132 # Create an option parser reading from +args+.
140 # Process all options.
148 when "master", "gateway", "client"
149 @options["mode"] = arg
150 when /^--(no-?)?(\w+)$/
151 negate
= (not $1.nil?)
153 if @
@known_options.include? option
154 type
= @
@known_options[option
]
156 if negate
and type
!= :boolean
157 raise Error
, "Tried to assign boolean value to " +
158 "non-boolean option: #{arg}"
159 elsif type
== :boolean
160 @options[option
] = (not negate
)
161 elsif type
== :integer
164 raise Error
, "Option needs a value: #{arg}"
166 @options[option
] = nil
167 elsif value
!~
/^\d+$/
168 raise Error
, "Tried to assign string value to " +
169 "integer option: #{arg}"
171 @options[option
] = value
.to_i
173 elsif type
== :string
176 raise Error
, "Option needs a value: #{arg}"
178 @options[option
] = nil
180 @options[option
] = value
184 raise Error
, "Unknown option: #{arg}"
189 process_yaml CLWHOME
+ "/profiles/" + arg
194 @options.each_pair
do |key
, value
|
195 if value
.is_a
? String
196 value
= value
.gsub(/\{(.*)\}/) {
200 when "~": ENV["HOME"] + "/.clw"
202 if @options["connect"].nil?
205 @options["connect"].split(":")[
206 ($1 == "host") ? 0 : 1
209 else @options[key
].to_s
212 @options[key
] = value
.empty
? ? nil : value
218 # Retrieve an option.
225 # Set an option manually.
227 def []=(option
, value
)
228 @options[option
] = value
234 # Process a YAML file to read options from.
236 def process_yaml(path
)
239 rescue SystemCallError
=> e
240 raise Error
, "While opening profile: #{e}"
244 yaml
= YAML
::load file
246 raise Error
, "#{path}: #{e}"
248 unless yaml
.is_a
? Hash
249 raise Error
, "#{path}: Hash expected"
252 yaml
.each_pair
do |key
, value
|
253 if @
@known_options.include? key
254 type
= @
@known_options[key
]
256 if value
.is_a
? String
and value
.empty
?
258 elsif type
== :boolean
259 unless value
== true or value
== false
260 raise Error
, "#{path}: #{key} is a boolean option"
262 @options[key
] = value
263 elsif type
== :integer
264 unless value
.is_a
? Fixnum
and value
>= 0
265 raise Error
, "#{path}: #{key} is a numeric option"
267 @options[key
] = value
268 elsif type
== :string
269 @options[key
] = value
.to_s
280 # Print an error message.
283 STDERR.puts
"Error: #{message}"
284 STDERR.puts
"Type `clw --help' for details."
291 options
= CommandLineParser
.new
ARGV
294 rescue CommandLineParser
::Error => e
304 mandatory
= ["ca", "cert", "key", "subject"]
306 case mode
= options
["mode"]
308 mandatory
+= ["gateway"]
310 # No additional mandatory options
312 mandatory
+= ["gateway", "connect"]
314 error
"Please specify a valid mode (master, gateway or client)."
319 mandatory
.each
do |option
|
320 if options
[option
].nil?
321 error
"--#{option} is mandatory for #{mode} operation."
327 subject
= options
["subject"]
330 ghost
, gport
= options
["gateway"].split(":")
332 gport
= Clarkway
::PORT
333 elsif gport
!~
/^\d+$/
334 error
"Gateway port has to be numeric."
340 reconnect
= options
["reconnect"] || 0
341 reconnect
= 60 if reconnect
== 0
344 daemon
= Clarkway
::Master.new(ghost
, gport
,
345 options
["subject"], reconnect
)
348 listen
= options
["listen"]
350 lhost
, lport
= nil, Clarkway
::PORT
352 lhost
, lport
= listen
.split(":")
353 lhost
, lport
= nil, lhost
if lport
.nil?
355 error
"Local port has to be numeric."
361 keepalive
= options
["keepalive"] || 0
362 keepalive
= nil if keepalive
== 0
364 require "clw/gateway"
365 daemon
= Clarkway
::Gateway.new(lhost
, lport
, subject
)
366 daemon
.keepalive
= keepalive
369 options
["daemon"] = false if options
["daemon"].nil?
371 # Where to forward to?
372 stdio
= options
["stdio"] || false
376 listen
= options
["listen"] || "0"
377 lhost
, lport
= listen
.split(":")
378 lhost
, lport
= nil, lhost
if lport
.nil?
380 error
"Local port has to be numeric."
384 lport
= nil if lport
== 0
387 # Where to connect to?
388 chost
, cport
= options
["connect"].split(":")
389 if cport
.nil? or cport
!~
/^\d+$/
390 error
"Target port has to be numeric."
395 ghost
, gport
= options
["gateway"].split(":")
397 gport
= Clarkway
::PORT
398 elsif gport
!~
/^\d+$/
399 error
"Gateway port has to be numeric."
405 # Command to execute afterwards
406 command
= options
["exec"]
408 options
["once"] = true
409 options
["daemon"] = false
413 direct
= options
["direct"]
416 dhost
, dport
= direct
.split
":"
419 error
"Direct connection port has to be numeric."
423 dport
= nil if dport
== 0
428 daemon
= Clarkway
::Client.new(chost
, cport
, ghost
, gport
, subject
)
429 daemon
.once
= options
["once"] || false
431 daemon
.exec
= command
432 daemon
.local_host
= lhost
unless lhost
.nil?
433 daemon
.local_port
= lport
434 daemon
.direct_host
= dhost
435 daemon
.direct_port
= dport
436 options
["daemon"] = false if stdio
440 cafile
= daemon
.ca
= options
["ca"]
442 ca
= OpenSSL
::X509::Certificate.new File
::read(cafile
)
443 daemon
.cert
= OpenSSL
::X509::Certificate.new File
::read(options
["cert"])
444 daemon
.key
= OpenSSL
::PKey::RSA.new File
::read(options
["key"])
445 unless options
["crl"].nil?
446 crl
= OpenSSL
::X509::CRL.new File
::read(options
["crl"])
447 raise "CRL not signed by CA" unless crl
.verify ca
.public_key
451 error
"While initializing OpenSSL: #{e}."
456 daemonize
= options
["daemon"]
457 daemonize
= true if daemonize
.nil?
458 logfile
= options
["logfile"]
459 loglevel
= options
["loglevel"]
460 logname
= options
["logname"] || "clw"
463 logfile
= daemonize
? nil : STDERR
466 elsif logfile
== "off"
471 daemon
.logger
= Logger
.new logfile
472 rescue SystemCallError
=> e
473 error
"While opening logfile: #{e}."
477 daemon
.logger
.progname
= logname
481 daemon
.logger
.sev_threshold
= Logger
::FATAL
483 daemon
.logger
.sev_threshold
= Logger
::WARN
485 daemon
.logger
.sev_threshold
= Logger
::INFO
489 pidfile
= options
["pidfile"]
492 oldpid
= File
::read(pidfile
).to_i
494 # Signal 0 does not kill, only check if process is alive:
495 Process
.kill
0, oldpid
499 error
"Already running (#{pidfile}, ##{oldpid})."
510 # PID file probably already deleted
518 pid
= daemon
.start
true if daemonize
521 file
= File
.new pidfile
, "w"
525 @logger.warn
"Unable to write PID file: #{e}."
528 daemon
.start
false unless daemonize
530 daemon
.start daemonize
537 exit main
if $0 == __FILE__