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 --connect HOST:PORT Host and port to connect to; mandatory for client,
58 --[no-]stdio Client uses standard input and output for the
59 communication with the target.
60 --listen [HOST:]PORT Gateway: Listen on host HOST [0.0.0.0], port PORT
62 Client: Listen on HOST [127.0.0.1], port PORT
65 --[no-]once Client closes server after first connection.
66 --[no-]daemon (Do not) daemonize after startup. Daemon mode is
67 enabled by default, except for clients acting in
69 --exec COMMAND Client runs COMMAND after opening server (if
70 --listen is active) or talks to it using a pipe (in
72 --pidfile FILE On startup, write process ID to FILE. If FILE
73 exists and creates a PID of a running process,
75 --logfile FILE Write logs to FILE. Default is to disable logging if
76 daemon mode is enabled, standard error output
77 otherwise. The special value `-' logs to stderr,
78 `off' forces to disable logging.
79 --loglevel LEVEL Minimum level (info, warn, fatal) of log messages.
80 Default is to log everything (info).
81 --logname NAME Log as program NAME. Useful if multiple instances
82 of Clarkway log into the same file.
83 --help Show this help.
85 Please report bugs to Michael Schutte <m.schutte.jr@gmail.com>.
92 class CommandLineParser
94 class Error
< RuntimeError
; end
96 attr_reader
:args, :options
98 CLWHOME
= ENV["HOME"] + "/.clw"
106 "subject" => :string,
107 "gateway" => :string,
108 "reconnect" => :integer,
109 "connect" => :string,
113 "daemon" => :boolean,
115 "pidfile" => :string,
116 "logfile" => :string,
117 "loglevel" => :string,
118 "logname" => :string,
123 # Create an option parser reading from +args+.
131 # Process all options.
139 when "master", "gateway", "client"
140 @options["mode"] = arg
141 when /^--(no-?)?(\w+)$/
142 negate
= (not $1.nil?)
144 if @
@known_options.include? option
145 type
= @
@known_options[option
]
147 if negate
and type
!= :boolean
148 raise Error
, "Tried to assign boolean value to " +
149 "non-boolean option: #{arg}"
150 elsif type
== :boolean
151 @options[option
] = (not negate
)
152 elsif type
== :integer
155 raise Error
, "Option needs a value: #{arg}"
157 @options[option
] = nil
158 elsif value
!~
/^\d+$/
159 raise Error
, "Tried to assign string value to " +
160 "integer option: #{arg}"
162 @options[option
] = value
.to_i
164 elsif type
== :string
167 raise Error
, "Option needs a value: #{arg}"
169 @options[option
] = nil
171 @options[option
] = value
175 raise Error
, "Unknown option: #{arg}"
180 process_yaml CLWHOME
+ "/profiles/" + arg
185 @options.each_pair
do |key
, value
|
186 if value
.is_a
? String
187 value
= value
.gsub(/\{(.*)\}/) {
191 when "~": ENV["HOME"] + "/.clw"
193 if @options["connect"].nil?
196 @options["connect"].split(":")[
197 ($1 == "host") ? 0 : 1
200 else @options[key
].to_s
203 @options[key
] = value
.empty
? ? nil : value
209 # Retrieve an option.
216 # Set an option manually.
218 def []=(option
, value
)
219 @options[option
] = value
225 # Process a YAML file to read options from.
227 def process_yaml(path
)
230 rescue SystemCallError
=> e
231 raise Error
, "While opening profile: #{e}"
235 yaml
= YAML
::load file
237 raise Error
, "#{path}: #{e}"
239 unless yaml
.is_a
? Hash
240 raise Error
, "#{path}: Hash expected"
243 yaml
.each_pair
do |key
, value
|
244 if @
@known_options.include? key
245 type
= @
@known_options[key
]
247 if value
.is_a
? String
and value
.empty
?
249 elsif type
== :boolean
250 unless value
== true or value
== false
251 raise Error
, "#{path}: #{key} is a boolean option"
253 @options[key
] = value
254 elsif type
== :integer
255 unless value
.is_a
? Fixnum
and value
>= 0
256 raise Error
, "#{path}: #{key} is a numeric option"
258 @options[key
] = value
259 elsif type
== :string
260 @options[key
] = value
.to_s
271 # Print an error message.
274 STDERR.puts
"Error: #{message}"
275 STDERR.puts
"Type `clw --help' for details."
282 options
= CommandLineParser
.new
ARGV
285 rescue CommandLineParser
::Error => e
295 mandatory
= ["ca", "cert", "key", "subject"]
297 case mode
= options
["mode"]
299 mandatory
+= ["gateway"]
301 # No additional mandatory options
303 mandatory
+= ["gateway", "connect"]
305 error
"Please specify a valid mode (master, gateway or client)."
310 mandatory
.each
do |option
|
311 if options
[option
].nil?
312 error
"--#{option} is mandatory for #{mode} operation."
318 subject
= options
["subject"]
321 ghost
, gport
= options
["gateway"].split(":")
323 gport
= Clarkway
::PORT
324 elsif gport
!~
/^\d+$/
325 error
"Gateway port has to be numeric."
331 reconnect
= options
["reconnect"] || 0
332 reconnect
= 60 if reconnect
== 0
335 daemon
= Clarkway
::Master.new(ghost
, gport
,
336 options
["subject"], reconnect
)
339 listen
= options
["listen"]
341 lhost
, lport
= nil, Clarkway
::PORT
343 lhost
, lport
= listen
.split(":")
344 lhost
, lport
= nil, lhost
if lport
.nil?
346 error
"Local port has to be numeric."
352 require "clw/gateway"
353 daemon
= Clarkway
::Gateway.new(lhost
, lport
, subject
)
356 options
["daemon"] = false if options
["daemon"].nil?
358 stdio
= options
["stdio"] || false
362 listen
= options
["listen"] || "0"
363 lhost
, lport
= listen
.split(":")
364 lhost
, lport
= nil, lhost
if lport
.nil?
366 error
"Local port has to be numeric."
370 lport
= nil if lport
== 0
373 chost
, cport
= options
["connect"].split(":")
374 if cport
.nil? or cport
!~
/^\d+$/
375 error
"Target port has to be numeric."
379 ghost
, gport
= options
["gateway"].split(":")
381 gport
= Clarkway
::PORT
382 elsif gport
!~
/^\d+$/
383 error
"Gateway port has to be numeric."
389 command
= options
["exec"]
391 options
["once"] = true
392 options
["daemon"] = false
396 daemon
= Clarkway
::Client.new(chost
, cport
, ghost
, gport
, subject
)
397 daemon
.once
= options
["once"] || false
399 daemon
.exec
= command
400 daemon
.localhost
= lhost
unless lhost
.nil?
401 daemon
.localport
= lport
402 options
["daemon"] = false if stdio
406 cafile
= daemon
.ca
= options
["ca"]
408 ca
= OpenSSL
::X509::Certificate.new File
::read(cafile
)
409 daemon
.cert
= OpenSSL
::X509::Certificate.new File
::read(options
["cert"])
410 daemon
.key
= OpenSSL
::PKey::RSA.new File
::read(options
["key"])
411 unless options
["crl"].nil?
412 crl
= OpenSSL
::X509::CRL.new File
::read(options
["crl"])
413 raise "CRL not signed by CA" unless crl
.verify ca
.public_key
417 error
"While initializing OpenSSL: #{e}."
422 daemonize
= options
["daemon"]
423 daemonize
= true if daemonize
.nil?
424 logfile
= options
["logfile"]
425 loglevel
= options
["loglevel"]
426 logname
= options
["logname"] || "clw"
429 logfile
= daemonize
? nil : STDERR
432 elsif logfile
== "off"
437 daemon
.logger
= Logger
.new logfile
438 rescue SystemCallError
=> e
439 error
"While opening logfile: #{e}."
443 daemon
.logger
.progname
= logname
447 daemon
.logger
.sev_threshold
= Logger
::FATAL
449 daemon
.logger
.sev_threshold
= Logger
::WARN
451 daemon
.logger
.sev_threshold
= Logger
::INFO
455 pidfile
= options
["pidfile"]
458 oldpid
= File
::read(pidfile
).to_i
460 # Signal 0 does not kill, only check if process is alive:
461 Process
.kill
0, oldpid
465 error
"Already running (#{pidfile}, ##{oldpid})."
476 # PID file probably already deleted
484 pid
= daemon
.start
true if daemonize
487 file
= File
.new pidfile
, "w"
491 @logger.warn
"Unable to write PID file: #{e}."
494 daemon
.start
false unless daemonize
496 daemon
.start daemonize
503 exit main
if $0 == __FILE__
505 # vim:tw=78:fmr=<<<,>>>:fdm=marker: