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 --direct HOST[:PORT] Client: Instead of redirecting traffic through the
66 gateway, listen on PORT and tell the master to
67 connect to HOST:PORT. HOST must be this host's
69 --[no-]once Client closes server after first connection.
70 --[no-]daemon (Do not) daemonize after startup. Daemon mode is
71 enabled by default, except for clients acting in
73 --exec COMMAND Client runs COMMAND after opening server (if
74 --listen is active) or talks to it using a pipe (in
76 --pidfile FILE On startup, write process ID to FILE. If FILE
77 exists and creates a PID of a running process,
79 --logfile FILE Write logs to FILE. Default is to disable logging if
80 daemon mode is enabled, standard error output
81 otherwise. The special value `-' logs to stderr,
82 `off' forces to disable logging.
83 --loglevel LEVEL Minimum level (info, warn, fatal) of log messages.
84 Default is to log everything (info).
85 --logname NAME Log as program NAME. Useful if multiple instances
86 of Clarkway log into the same file.
87 --help Show this help.
89 Please report bugs to Michael Schutte <m.schutte.jr@gmail.com>.
96 class CommandLineParser
98 class Error
< RuntimeError
; end
100 attr_reader
:args, :options
102 CLWHOME
= ENV["HOME"] + "/.clw"
110 "subject" => :string,
111 "gateway" => :string,
112 "reconnect" => :integer,
113 "connect" => :string,
118 "daemon" => :boolean,
120 "pidfile" => :string,
121 "logfile" => :string,
122 "loglevel" => :string,
123 "logname" => :string,
128 # Create an option parser reading from +args+.
136 # Process all options.
144 when "master", "gateway", "client"
145 @options["mode"] = arg
146 when /^--(no-?)?(\w+)$/
147 negate
= (not $1.nil?)
149 if @
@known_options.include? option
150 type
= @
@known_options[option
]
152 if negate
and type
!= :boolean
153 raise Error
, "Tried to assign boolean value to " +
154 "non-boolean option: #{arg}"
155 elsif type
== :boolean
156 @options[option
] = (not negate
)
157 elsif type
== :integer
160 raise Error
, "Option needs a value: #{arg}"
162 @options[option
] = nil
163 elsif value
!~
/^\d+$/
164 raise Error
, "Tried to assign string value to " +
165 "integer option: #{arg}"
167 @options[option
] = value
.to_i
169 elsif type
== :string
172 raise Error
, "Option needs a value: #{arg}"
174 @options[option
] = nil
176 @options[option
] = value
180 raise Error
, "Unknown option: #{arg}"
185 process_yaml CLWHOME
+ "/profiles/" + arg
190 @options.each_pair
do |key
, value
|
191 if value
.is_a
? String
192 value
= value
.gsub(/\{(.*)\}/) {
196 when "~": ENV["HOME"] + "/.clw"
198 if @options["connect"].nil?
201 @options["connect"].split(":")[
202 ($1 == "host") ? 0 : 1
205 else @options[key
].to_s
208 @options[key
] = value
.empty
? ? nil : value
214 # Retrieve an option.
221 # Set an option manually.
223 def []=(option
, value
)
224 @options[option
] = value
230 # Process a YAML file to read options from.
232 def process_yaml(path
)
235 rescue SystemCallError
=> e
236 raise Error
, "While opening profile: #{e}"
240 yaml
= YAML
::load file
242 raise Error
, "#{path}: #{e}"
244 unless yaml
.is_a
? Hash
245 raise Error
, "#{path}: Hash expected"
248 yaml
.each_pair
do |key
, value
|
249 if @
@known_options.include? key
250 type
= @
@known_options[key
]
252 if value
.is_a
? String
and value
.empty
?
254 elsif type
== :boolean
255 unless value
== true or value
== false
256 raise Error
, "#{path}: #{key} is a boolean option"
258 @options[key
] = value
259 elsif type
== :integer
260 unless value
.is_a
? Fixnum
and value
>= 0
261 raise Error
, "#{path}: #{key} is a numeric option"
263 @options[key
] = value
264 elsif type
== :string
265 @options[key
] = value
.to_s
276 # Print an error message.
279 STDERR.puts
"Error: #{message}"
280 STDERR.puts
"Type `clw --help' for details."
287 options
= CommandLineParser
.new
ARGV
290 rescue CommandLineParser
::Error => e
300 mandatory
= ["ca", "cert", "key", "subject"]
302 case mode
= options
["mode"]
304 mandatory
+= ["gateway"]
306 # No additional mandatory options
308 mandatory
+= ["gateway", "connect"]
310 error
"Please specify a valid mode (master, gateway or client)."
315 mandatory
.each
do |option
|
316 if options
[option
].nil?
317 error
"--#{option} is mandatory for #{mode} operation."
323 subject
= options
["subject"]
326 ghost
, gport
= options
["gateway"].split(":")
328 gport
= Clarkway
::PORT
329 elsif gport
!~
/^\d+$/
330 error
"Gateway port has to be numeric."
336 reconnect
= options
["reconnect"] || 0
337 reconnect
= 60 if reconnect
== 0
340 daemon
= Clarkway
::Master.new(ghost
, gport
,
341 options
["subject"], reconnect
)
344 listen
= options
["listen"]
346 lhost
, lport
= nil, Clarkway
::PORT
348 lhost
, lport
= listen
.split(":")
349 lhost
, lport
= nil, lhost
if lport
.nil?
351 error
"Local port has to be numeric."
357 require "clw/gateway"
358 daemon
= Clarkway
::Gateway.new(lhost
, lport
, subject
)
361 options
["daemon"] = false if options
["daemon"].nil?
363 # Where to forward to?
364 stdio
= options
["stdio"] || false
368 listen
= options
["listen"] || "0"
369 lhost
, lport
= listen
.split(":")
370 lhost
, lport
= nil, lhost
if lport
.nil?
372 error
"Local port has to be numeric."
376 lport
= nil if lport
== 0
379 # Where to connect to?
380 chost
, cport
= options
["connect"].split(":")
381 if cport
.nil? or cport
!~
/^\d+$/
382 error
"Target port has to be numeric."
387 ghost
, gport
= options
["gateway"].split(":")
389 gport
= Clarkway
::PORT
390 elsif gport
!~
/^\d+$/
391 error
"Gateway port has to be numeric."
397 # Command to execute afterwards
398 command
= options
["exec"]
400 options
["once"] = true
401 options
["daemon"] = false
405 direct
= options
["direct"]
408 dhost
, dport
= direct
.split
":"
411 error
"Direct connection port has to be numeric."
415 dport
= nil if dport
== 0
420 daemon
= Clarkway
::Client.new(chost
, cport
, ghost
, gport
, subject
)
421 daemon
.once
= options
["once"] || false
423 daemon
.exec
= command
424 daemon
.local_host
= lhost
unless lhost
.nil?
425 daemon
.local_port
= lport
426 daemon
.direct_host
= dhost
427 daemon
.direct_port
= dport
428 options
["daemon"] = false if stdio
432 cafile
= daemon
.ca
= options
["ca"]
434 ca
= OpenSSL
::X509::Certificate.new File
::read(cafile
)
435 daemon
.cert
= OpenSSL
::X509::Certificate.new File
::read(options
["cert"])
436 daemon
.key
= OpenSSL
::PKey::RSA.new File
::read(options
["key"])
437 unless options
["crl"].nil?
438 crl
= OpenSSL
::X509::CRL.new File
::read(options
["crl"])
439 raise "CRL not signed by CA" unless crl
.verify ca
.public_key
443 error
"While initializing OpenSSL: #{e}."
448 daemonize
= options
["daemon"]
449 daemonize
= true if daemonize
.nil?
450 logfile
= options
["logfile"]
451 loglevel
= options
["loglevel"]
452 logname
= options
["logname"] || "clw"
455 logfile
= daemonize
? nil : STDERR
458 elsif logfile
== "off"
463 daemon
.logger
= Logger
.new logfile
464 rescue SystemCallError
=> e
465 error
"While opening logfile: #{e}."
469 daemon
.logger
.progname
= logname
473 daemon
.logger
.sev_threshold
= Logger
::FATAL
475 daemon
.logger
.sev_threshold
= Logger
::WARN
477 daemon
.logger
.sev_threshold
= Logger
::INFO
481 pidfile
= options
["pidfile"]
484 oldpid
= File
::read(pidfile
).to_i
486 # Signal 0 does not kill, only check if process is alive:
487 Process
.kill
0, oldpid
491 error
"Already running (#{pidfile}, ##{oldpid})."
502 # PID file probably already deleted
510 pid
= daemon
.start
true if daemonize
513 file
= File
.new pidfile
, "w"
517 @logger.warn
"Unable to write PID file: #{e}."
520 daemon
.start
false unless daemonize
522 daemon
.start daemonize
529 exit main
if $0 == __FILE__
531 # vim:tw=78:fmr=<<<,>>>:fdm=marker: