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 --direct-ports LIST Master: Only directly connect to ports found in
73 LIST. Useful when a firewall is filtering ports.
74 --[no-]once Client closes server after first connection.
75 --[no-]daemon (Do not) daemonize after startup. Daemon mode is
76 enabled by default, except for clients acting in
78 --exec COMMAND Client runs COMMAND after opening server (if
79 --listen is active) or talks to it using a pipe (in
81 --pidfile FILE On startup, write process ID to FILE. If FILE
82 exists and creates a PID of a running process,
84 --logfile FILE Write logs to FILE. Default is to disable logging if
85 daemon mode is enabled, standard error output
86 otherwise. The special value `-' logs to stderr,
87 `off' forces to disable logging.
88 --loglevel LEVEL Minimum level (info, warn, fatal) of log messages.
89 Default is to log everything (info).
90 --logname NAME Log as program NAME. Useful if multiple instances
91 of Clarkway log into the same file.
92 --help Show this help.
94 Please report bugs to Michael Schutte <m.schutte.jr@gmail.com>.
101 class CommandLineParser
103 class Error
< RuntimeError
; end
105 attr_reader
:args, :options
107 CLWHOME
= ENV["HOME"] + "/.clw"
115 "subject" => :string,
116 "gateway" => :string,
117 "reconnect" => :integer,
118 "keepalive" => :integer,
119 "connect" => :string,
123 "direct-ports" => :string,
125 "daemon" => :boolean,
127 "pidfile" => :string,
128 "logfile" => :string,
129 "loglevel" => :string,
130 "logname" => :string,
135 # Create an option parser reading from +args+.
143 # Process all options.
151 when "master", "gateway", "client"
152 @options["mode"] = arg
153 when /^--(no-?)?([a-zA-Z-]+)$/
154 negate
= (not $1.nil?)
156 if @
@known_options.include? option
157 type
= @
@known_options[option
]
159 if negate
and type
!= :boolean
160 raise Error
, "Tried to assign boolean value to " +
161 "non-boolean option: #{arg}"
162 elsif type
== :boolean
163 @options[option
] = (not negate
)
164 elsif type
== :integer
167 raise Error
, "Option needs a value: #{arg}"
169 @options[option
] = nil
170 elsif value
!~
/^\d+$/
171 raise Error
, "Tried to assign string value to " +
172 "integer option: #{arg}"
174 @options[option
] = value
.to_i
176 elsif type
== :string
179 raise Error
, "Option needs a value: #{arg}"
181 @options[option
] = nil
183 @options[option
] = value
187 raise Error
, "Unknown option: #{arg}"
192 process_yaml CLWHOME
+ "/profiles/" + arg
197 @options.each_pair
do |key
, value
|
198 if value
.is_a
? String
199 value
= value
.gsub(/\{(.*)\}/) {
203 when "~": ENV["HOME"] + "/.clw"
205 if @options["connect"].nil?
208 @options["connect"].split(":")[
209 ($1 == "host") ? 0 : 1
212 else @options[key
].to_s
215 @options[key
] = value
.empty
? ? nil : value
221 # Retrieve an option.
228 # Set an option manually.
230 def []=(option
, value
)
231 @options[option
] = value
237 # Process a YAML file to read options from.
239 def process_yaml(path
)
242 rescue SystemCallError
=> e
243 raise Error
, "While opening profile: #{e}"
247 yaml
= YAML
::load file
249 raise Error
, "#{path}: #{e}"
251 unless yaml
.is_a
? Hash
252 raise Error
, "#{path}: Hash expected"
255 yaml
.each_pair
do |key
, value
|
256 if @
@known_options.include? key
257 type
= @
@known_options[key
]
259 if value
.is_a
? String
and value
.empty
?
261 elsif type
== :boolean
262 unless value
== true or value
== false
263 raise Error
, "#{path}: #{key} is a boolean option"
265 @options[key
] = value
266 elsif type
== :integer
267 unless value
.is_a
? Fixnum
and value
>= 0
268 raise Error
, "#{path}: #{key} is a numeric option"
270 @options[key
] = value
271 elsif type
== :string
272 @options[key
] = value
.to_s
283 # Print an error message.
286 STDERR.puts
"Error: #{message}"
287 STDERR.puts
"Type `clw --help' for details."
294 options
= CommandLineParser
.new
ARGV
297 rescue CommandLineParser
::Error => e
307 mandatory
= ["ca", "cert", "key", "subject"]
309 case mode
= options
["mode"]
311 mandatory
+= ["gateway"]
313 # No additional mandatory options
315 mandatory
+= ["gateway", "connect"]
317 error
"Please specify a valid mode (master, gateway or client)."
322 mandatory
.each
do |option
|
323 if options
[option
].nil?
324 error
"--#{option} is mandatory for #{mode} operation."
330 subject
= options
["subject"]
333 ghost
, gport
= options
["gateway"].split(":")
335 gport
= Clarkway
::PORT
336 elsif gport
!~
/^\d+$/
337 error
"Gateway port has to be numeric."
343 reconnect
= options
["reconnect"] || 0
344 reconnect
= 60 if reconnect
== 0
347 daemon
= Clarkway
::Master.new(ghost
, gport
,
348 options
["subject"], reconnect
)
350 direct_ports
= options
["direct-ports"]
351 if direct_ports
.nil? or direct_ports
.empty
?
352 daemon
.direct_ports
= nil
353 elsif direct_ports
== "-"
354 daemon
.direct_ports
= []
356 daemon
.direct_ports
= []
357 direct_ports
.split(",").each
do |port
|
358 if port
=~
/^(\d+)\.\.(\d+)$/
359 daemon
.direct_ports
<< $1.to_i
.. $2.to_i
360 elsif port
=~
/^(\d+)$/
361 daemon
.direct_ports
<< $1.to_i
363 error
"Invalid direct port range #{port}."
370 listen
= options
["listen"]
372 lhost
, lport
= nil, Clarkway
::PORT
374 lhost
, lport
= listen
.split(":")
375 lhost
, lport
= nil, lhost
if lport
.nil?
377 error
"Local port has to be numeric."
383 keepalive
= options
["keepalive"] || 0
384 keepalive
= nil if keepalive
== 0
386 require "clw/gateway"
387 daemon
= Clarkway
::Gateway.new(lhost
, lport
, subject
)
388 daemon
.keepalive
= keepalive
391 options
["daemon"] = false if options
["daemon"].nil?
393 # Where to forward to?
394 stdio
= options
["stdio"] || false
398 listen
= options
["listen"] || "0"
399 lhost
, lport
= listen
.split(":")
400 lhost
, lport
= nil, lhost
if lport
.nil?
402 error
"Local port has to be numeric."
406 lport
= nil if lport
== 0
409 # Where to connect to?
410 chost
, cport
= options
["connect"].split(":")
411 if cport
.nil? or cport
!~
/^\d+$/
412 error
"Target port has to be numeric."
417 ghost
, gport
= options
["gateway"].split(":")
419 gport
= Clarkway
::PORT
420 elsif gport
!~
/^\d+$/
421 error
"Gateway port has to be numeric."
427 # Command to execute afterwards
428 command
= options
["exec"]
430 options
["once"] = true
431 options
["daemon"] = false
435 direct
= options
["direct"]
438 dhost
, dport
= direct
.split
":"
441 error
"Direct connection port has to be numeric."
445 dport
= nil if dport
== 0
450 daemon
= Clarkway
::Client.new(chost
, cport
, ghost
, gport
, subject
)
451 daemon
.once
= options
["once"] || false
453 daemon
.exec
= command
454 daemon
.local_host
= lhost
unless lhost
.nil?
455 daemon
.local_port
= lport
456 daemon
.direct_host
= dhost
457 daemon
.direct_port
= dport
458 options
["daemon"] = false if stdio
462 cafile
= daemon
.ca
= options
["ca"]
464 ca
= OpenSSL
::X509::Certificate.new File
::read(cafile
)
465 daemon
.cert
= OpenSSL
::X509::Certificate.new File
::read(options
["cert"])
466 daemon
.key
= OpenSSL
::PKey::RSA.new File
::read(options
["key"])
467 unless options
["crl"].nil?
468 crl
= OpenSSL
::X509::CRL.new File
::read(options
["crl"])
469 raise "CRL not signed by CA" unless crl
.verify ca
.public_key
473 error
"While initializing OpenSSL: #{e}."
478 daemonize
= options
["daemon"]
479 daemonize
= true if daemonize
.nil?
480 logfile
= options
["logfile"]
481 loglevel
= options
["loglevel"]
482 logname
= options
["logname"] || "clw"
485 logfile
= daemonize
? nil : STDERR
488 elsif logfile
== "off"
493 daemon
.logger
= Logger
.new logfile
494 rescue SystemCallError
=> e
495 error
"While opening logfile: #{e}."
499 daemon
.logger
.progname
= logname
503 daemon
.logger
.sev_threshold
= Logger
::FATAL
505 daemon
.logger
.sev_threshold
= Logger
::WARN
507 daemon
.logger
.sev_threshold
= Logger
::INFO
511 pidfile
= options
["pidfile"]
514 oldpid
= File
::read(pidfile
).to_i
516 # Signal 0 does not kill, only check if process is alive:
517 Process
.kill
0, oldpid
521 error
"Already running (#{pidfile}, ##{oldpid})."
532 # PID file probably already deleted
540 pid
= daemon
.start
true if daemonize
543 file
= File
.new pidfile
, "w"
547 @logger.warn
"Unable to write PID file: #{e}."
550 daemon
.start
false unless daemonize
552 daemon
.start daemonize
559 exit main
if $0 == __FILE__