#!/usr/bin/env python
"""Script to connect your machine to SUSE IPsec RAS service"
See print_help() below for a longer description."""
#__revision__ = "$Id: ras.py,v 1.53 2005/07/19 20:15:41 garloff Exp $"
__revision__ = "@hg_rev:173@"
# (c) Kurt Garloff <garloff@suse.de>, 2005-04-17, Python license
#
# TODO:
# * Track status in config file specific lock/logfiles, so more than one
# instance can run.
# * PyQt GUI.
# * Allow doing telnet via ssh as well.
import sys, os, time, re, signal, errno
import ConfigParser
# Colorize
esc = chr(27)
red = esc + '[0;31m'
green = esc + '[0;32m'
yellow = esc + '[0;33m'
bold = esc + '[0;1m'
norm = esc + '[0;0m'
redraspy = red + 'ras.py:' + norm
greenraspy = green + 'ras.py:' + norm
yellowraspy = yellow + 'ras.py:' + norm
# Global Vars
passphrase = ''
cfg = ConfigParser.RawConfigParser()
configfile = '/etc/ras.config'
timefmt = "%a %Y-%m-%d %H:%M:%S"
interrupted = 0; reconnect = 0
verbose = 0; quiet = 0
outvfn = None; outqfn = sys.stdout.fileno()
daemon = 0
alreadyauth = 0
connections = []
iswhiterussian = False
# Global vars for the daemon
lockfilename = '/var/run/ras.pid'
logfilename = '/var/log/ras-messages'
NEWRASSESSION = '*** NEW RAS SESSION ***'
BEGINRASSTATUS = '*** BEGIN RAS STATUS ***'
ENDRASSTATUS = '*** END RAS STATUS ***'
global_kwds = ('pinginterval', 'pingtimeout', 'pingfail', 'failholdoff',
'maxconnect', 'authhost', 'authretry', 'logname',
'virtip', 'netdev', 'gateway',
'ipseccmd', 'ipsecinit', 'ipsecconfread',
'ipcmd', 'telnetcmd', 'routeraw', 'netdevexist',
'cmdprefix')
conn_kwds = ('route', 'mtu', 'pinghost', 'desc')
def create_lockfile():
"Creates lock file and writes pid to it"
lockfd = os.open(lockfilename, os.O_WRONLY|os.O_CREAT|os.O_TRUNC)
# No O_EXCL needed, the location is not world writable
if not lockfd:
print redraspy, "Can't create lock file %s" % lockfilename
sys.exit(1)
os.write(lockfd, "%i\n" % os.getpid())
def check_lockfile():
"Checks for lock file and returns pid if present and if process exists"
try:
lfile = open(lockfilename)
except:
return 0
txt = lfile.readline()
dpid = int(txt)
lfile.close()
if not os.access('/proc/%i' % dpid, os.R_OK):
print yellowraspy, "removing stale lockfile for pid %i" % dpid
os.remove(lockfilename)
dpid = 0
return dpid
def del_lockfile():
"Removes lock file"
dpid = check_lockfile()
if not dpid:
return
if dpid != os.getpid():
print yellowraspy, "Won't remove lock file for pid %i, self %i" % \
(dpid, os.getpid())
os.remove(lockfilename)
def daemonize():
"Fork and exit parent. Child closes fds and logs to /var/log/ras-messages"
if os.fork():
os._exit(0)
create_lockfile()
os.setsid()
#os.umask(077)
for i in range(3):
try:
os.close(i)
except OSError, err:
if err.errno != errno.EBADF:
raise
os.open('/dev/null', os.O_RDONLY) # stdin
os.open(logfilename, os.O_WRONLY|os.O_CREAT|os.O_APPEND)
os.dup(1) # stdout and stderr
signal.signal(signal.SIGHUP, sighandler_reconnect)
print NEWRASSESSION
def del_env(name):
"Delete environment variables"
try:
del os.environ[name]
except:
pass
def sanitize_env():
"Make environment safe for root use, just in case ..."
os.environ['PATH'] = '/sbin:/bin:/usr/sbin:/usr/bin'
os.environ['SHELL'] = '/bin/sh'
os.environ['LC_ALL'] = 'POSIX'
del_env('LD_PRELOAD')
del_env('LD_LIBRARY_PATH')
del_env('LD_RUN_PATH')
del_env('MALLOC_CHECK_')
# We close all fds in daemonize()
# Signal safe variants of time.sleep() and fdes.read()
def my_sleep(secs):
"""Sleep secs seconds, but continue sleeping after receiving signal,
don't sleep if interrupted is set."""
starttm = time.time()
#time.sleep(secs)
while not interrupted and not reconnect:
tosleep = starttm + secs - time.time()
if (tosleep <= 0):
break
time.sleep(tosleep)
def my_read(fdes):
"Read from file descriptor fdes, but restart on EINTR"
txt = ''
while not interrupted:
try:
txt += fdes.read()
return txt
except IOError, err:
if err.errno in (errno.EINTR, errno.EAGAIN):
continue
else:
raise
def my_read_until(sess, what, tout = None):
"sess.read_until with safety against EINTR"
import socket, select
txt = ''
while 1:
try:
txt += sess.read_until(what, tout)
return txt
except socket.error, err:
if err.args[0] in (errno.EINTR, errno.EAGAIN):
continue
else:
raise
except select.error, err:
if err.args[0] in (errno.EINTR, errno.EAGAIN):
continue
else:
raise
def my_read_some(sess):
"sess.read_some with safety against EINTR"
import socket, select
while 1:
try:
txt = sess.read_some()
return txt
except socket.error, err:
#print err
if err.args[0] in (errno.EINTR, errno.EAGAIN):
continue
else:
raise
except select.error, err:
if err.args[0] in (errno.EINTR, errno.EAGAIN):
continue
else:
raise
def write_all(fdes, data):
"Output data to fdes"
while data:
written = os.write(fdes, data)
data = data[written:]
def read_until(fdes, search, timeout = 4, fdout = None):
"""Read from fdes and wait for a list of strings (search) or EOF.
Echos read data to fdout (if specified), returns all captured
text and a status code:
0 .. N-1: string that matched
-1 : EOF
-2 : timeout
-X : Other error
Notes: fdes and fdout are file numbers not objects."""
import select
rdbuf = ''
#print 'read_until(%i, %s, %i, %i)' % (fdes, search, timeout, fdout)
while 1:
try:
rfd, wfd, xfd = select.select([fdes], [], [fdes], timeout)
except select.error, err:
# Hmm, why does select not raise OSError?
if err.args[0] in (errno.EINTR, errno.EAGAIN):
continue
else:
raise
# Timeout
if not fdes in rfd and not fdes in xfd:
return rdbuf, -2
#print yellowraspy, 'Timeout waiting for data'
read = os.read(fdes, 1024)
if not read:
# EOF
return rdbuf, -1
rdbuf += read
if fdout:
write_all(fdout, read)
# Search for strings
idx = 0
for sstr in search:
if sstr in rdbuf:
return rdbuf, idx
idx += 1
def check_exec(enm):
"Check for executable enm"
if not os.access(enm, os.X_OK):
print redraspy, "Fatal: Can't execute %s" % enm
sys.exit(5)
def check_prereq():
"Check for the existance of required executables"
check_exec('/usr/bin/opiekey')
check_exec('/bin/ping')
# FIXME: This is fuzzy ...
if not cfg.has_option('global', 'cmdprefix'):
check_exec('/sbin/ip')
check_exec('/etc/init.d/ipsec')
check_exec('/usr/sbin/ipsec')
def check_whiterussian():
"""Check whether we are running on OpenWRT (white russian)
WhiteRussian 0.9 has a number of flaws in python (segfaults
on importing platform, telnetlib/sockets not working, getpass
not working ...
This detects it and enables the workarounds."""
global iswhiterussian
iswhiterussian = False
try:
banner = open('/etc/banner', 'r')
except:
return
wrre = re.compile(r'WHITE RUSSIAN \(0\.[0-9]')
for ln in banner.readlines():
if wrre.search(ln):
iswhiterussian = True
return
def pass_timeout(sig, frame):
"Timeout for password handling"
print '\n', redraspy, 'Timeout waiting for OPIE Passphrase'
#signal.signal(sig, signal.SIG_DFL)
os._exit(128+sig)
def mygetpass(prompt = "Password: ", safe = True):
import termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
new = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ECHO # lflags
try:
termios.tcsetattr(fd, termios.TCSADRAIN, new)
except:
if safe:
raise
os.popen('stty -echo')
try:
passwd = raw_input(prompt)
finally:
try:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
except:
os.popen('stty echo')
print '\n',
return passwd
def input_passphrase():
"Query OPIE passphrase"
#import getpass
global passphrase
signal.alarm(60)
signal.signal(signal.SIGALRM, pass_timeout)
passphrase = mygetpass(greenraspy + ' Enter Passphrase for OPIE: ', False)
signal.alarm(0)
cfg_defaults = {'pinginterval': 20, 'pingtimeout': 4,
'pingfail': 4, 'failholdoff': 150, 'authretry': 3,
'ipseccmd': '/usr/sbin/ipsec auto',
'ipsecinit': '/etc/init.d/ipsec',
'ipsecconfread': 'cat /etc/ipsec.conf',
'ipcmd': '/sbin/ip',
'telnetcmd': '/usr/bin/telnet',
'routeraw': 'cat /proc/net/route',
'netdevexist': 'test -e /proc/sys/net/ipv4/conf/'}
def set_config_defaults():
"Fill in defaults into the global section"
for key in cfg_defaults.keys():
if not cfg.has_option('global', key):
cfg.set('global', key, cfg_defaults[key])
def hextoip(hstr):
"Convert netw byte order hex number in hstr to ip addr notation"
ipno = int(hstr, 16)
return '%i.%i.%i.%i' % (ipno & 0xff, (ipno >> 8) & 0xff,
(ipno >> 16) & 0xff, (ipno >> 24) & 0xff)
def set_config_netdev():
"Figure out the IPsec device of the default route"
# For a kernel with KLIPS, assume ipsec0 is good
if os.popen('%s%s' % \
(cfg.get('global', 'netdevexist'), 'ipsec0')
).close() == None:
cfg.set('global', 'netdev', 'ipsec0')
else:
# Parse /proc/net/route for the default route
rfd = os.popen('%s' % cfg.get('global', 'routeraw'))
for line in rfd.readlines():
splitline = line.split('\t')
if splitline[1] == '00000000':
cfg.set('global', 'netdev', splitline[0])
if not cfg.has_option('global', 'gateway'):
cfg.set('global', 'gateway',
'via %s' % hextoip(splitline[2]))
rfd.close()
return
rfd.close()
print redraspy, 'Fatal: No netdev specified and no default route'
sys.exit(1)
def cfg_prefix(optname, prefix):
"Prefix a global cfg option optname with prefix"
opt = cfg.get('global', optname)
cfg.set('global', optname, prefix + ' ' + opt)
def read_config(cfgfile, reset = 0):
"""Read config file (specified by cfgfile) and
fill in defaults; store in global var cfg"""
#global cfg
if not os.access(cfgfile, os.R_OK):
print redraspy, "Fatal: Config file %s can't be accessed" % cfgfile
sys.exit(6)
if reset:
logname = cfg.get('global', 'logname')
for sect in cfg.sections():
cfg.remove_section(sect)
else:
logname = os.getlogin()
cfg.read(cfgfile)
if not cfg.has_section('global'):
print redraspy, 'Fatal: No [global] section found in %s' % cfgfile
sys.exit(6)
set_config_defaults()
if cfg.has_option('global', 'cmdprefix'):
prefix = cfg.get('global', 'cmdprefix')
cfg_prefix('ipseccmd', prefix)
cfg_prefix('ipsecinit', prefix)
cfg_prefix('ipsecconfread', prefix)
cfg_prefix('ipcmd', prefix)
cfg_prefix('telnetcmd', prefix)
cfg_prefix('routeraw', prefix)
cfg_prefix('netdevexist', prefix)
cfg.read(cfgfile)
set_config_defaults()
if len(cfg.sections()) < 2:
print redraspy, 'Fatal: No tunnels configured in %s' % cfgfile
sys.exit(6)
if not cfg.has_option('global', 'netdev'):
set_config_netdev()
if not cfg.has_option('global', 'gateway'):
cfg.set('global', 'gateway', 'scope link')
if not cfg.has_option('global', 'logname'):
cfg.set('global', 'logname', logname)
#if verbose: print cfg.__dict__
# Check for unknown options!
for opt in cfg.options('global'):
if not opt in global_kwds:
print yellowraspy, "Unknown option %s in global section" % opt
def get_opie_key(challenge):
"""Call opiekey, give it the challenge and the passphrase,
get back and return response."""
import popen2
print challenge
chall = challenge.split(' ')
if not quiet:
if int(chall[1]) < 50:
print yellowraspy, "Renew your OPIE key, only %i logins left" \
% int(chall[1])
else:
print greenraspy, "%i logins left" % int(chall[1])
opie = popen2.Popen4('/usr/bin/opiekey %s %s %s' % \
(chall[1], chall[2], chall[3]))
fdes = opie.fromchild.fileno()
txt, tout = read_until(fdes, ("pass phrase: ",), 4, outvfn)
if tout < 0:
print redraspy, 'Fatal: Timeout waiting for opiekey'
sys.exit(1)
# sys.stdout.flush()
opie.tochild.write(passphrase + '\n')
opie.tochild.flush()
# opie.tochild.close()
# Ignore one linefeed
ret = opie.fromchild.readline()
# The answer ...
ret = my_read(opie.fromchild)
excode = opie.wait()
if verbose:
print " %i " % excode,
return ret
def auth_session_telnetlib():
"Do telnet to authhost for opie authentication via telnetlib"
import telnetlib
#global verbose
host = cfg.get('global', 'authhost')
if not quiet:
print greenraspy, 'Trying auth session to %s' % host
auth = 0
authretry = cfg.get('global', 'authretry')
while not authretry or auth < authretry:
try:
sess = telnetlib.Telnet(host)
break
except telnetlib.socket.error, err:
auth += 1
print yellowraspy, str(err)
if authretry and auth < authretry:
my_sleep(int(cfg.get('global', 'failholdoff')))
else:
print redraspy, "Fatal: Can't connect to %s" % host
return 1
txt = my_read_until(sess, 'login: ', 32)
if verbose:
print txt
if not ('login: ' in txt) or interrupted:
print redraspy, 'Fatal: Login prompt not found'
sess.close()
return 1
#time.sleep(0.5)
sess.write(cfg.get('global', 'logname') + '\n')
txt = my_read_until(sess, 'Response: ', 16)
if verbose:
print txt
resp = get_opie_key(txt.split('\n')[0])
sess.write(resp)
if verbose:
print resp
txt = my_read_some(sess)
while txt:
if verbose:
sys.stdout.write(txt)
if 'Login incorrect' in txt or interrupted:
print redraspy, 'Fatal: Authentication failed'
sess.close()
return 1
txt = my_read_some(sess)
sess.close()
return 0
def auth_session():
"Do telnet to authhost for opie authentication"
telnetcmd = cfg.get('global', 'telnetcmd')
# Handle OpenWRT specially; it's telnetlib is broken
if telnetcmd == '/usr/bin/telnet' and not iswhiterussian:
return auth_session_telnetlib()
import popen2, signal
#global verbose
host = cfg.get('global', 'authhost')
if not quiet:
print greenraspy, 'Trying auth session to %s' % host
auth = 0
authretry = cfg.get('global', 'authretry')
while not authretry or auth < authretry:
sess = popen2.Popen3("%s %s" % \
(telnetcmd, host), True)
# Allow for nameserver or refused conn. to show
if sess.poll() == -1:
time.sleep(0.5)
#print sess.__dict__
if sess.poll() == -1:
break
auth += 1
print yellowraspy, sess.childerr.read()
if authretry and auth < authretry:
my_sleep(int(cfg.get('global', 'failholdoff')))
else:
print redraspy, "Fatal: Can't connect to %s" % host
return 1
fromfd = sess.fromchild.fileno()
txt, tout = read_until(fromfd, ('login: ', ), 32) #, sys.stdout.fileno())
if verbose:
print txt
if not ('login: ' in txt) or interrupted:
exitstat = sess.poll()
exiterr = os.read(sess.childerr.fileno(), 4096)
print redraspy, 'Fatal: Login prompt not found (%i/%i)\n %s' % \
(tout, exitstat, exiterr)
if exitstat == -1:
os.kill(sess.pid, signal.SIGHUP)
return 1
#time.sleep(0.5)
os.write(sess.tochild.fileno(), cfg.get('global', 'logname') + '\n')
#print cfg.get('global', 'logname')
txt, tout = read_until(fromfd, ('Response: ', ), 16, sys.stdout.fileno())
if verbose:
print txt
resp = get_opie_key(txt.split('\n')[1])
os.write(sess.tochild.fileno(), resp + '\n')
#if verbose:
# print resp
exitstat = sess.poll()
while exitstat == -1:
txt = os.read(fromfd, 1024)
if verbose:
sys.stdout.write(txt)
if 'Login incorrect' in txt or interrupted:
print redraspy, 'Fatal: Authentication failed'
if sess.poll() == -1:
os.kill(sess.pid, signal.SIGHUP)
return 1
exitstat = sess.poll()
if exitstat != 0 and verbose:
print yellowraspy, 'Info: telnet exit status %i' % exitstat
return 0
def ipsec_parse(lines, conn, tag):
"Find tag in conn, also understanding also= include syntax"
inconn = 0
also = []
reconn = re.compile('[ \t]*conn[ \t]*([^ \t]*)[ \t]*$')
retag = re.compile('[ \t]+%s[ \t]*=[ \t]*(.*)$' % tag)
realso = re.compile('[ \t]+also[ \t]*=[ \t]*(.*)$')
for line in lines:
cmatch = reconn.match(line)
if cmatch:
if cmatch.group(1) == conn:
inconn = 1
else:
inconn = 0
continue
if not inconn:
continue
tmatch = retag.match(line)
#print line, tmatch
if tmatch:
return tmatch.group(1)
amatch = realso.match(line)
if amatch:
also.append(amatch.group(1))
# If we got here, we need to scan the also= sections
for sect in also:
ret = ipsec_parse(lines, sect, tag)
if ret:
return ret
# Not found, error
return None
def extract_route(connname):
"Fill find route in ipsec.conf, assuming we are left"
ipsecconf = os.popen(cfg.get('global', 'ipsecconfread'), 'r').readlines()
# Strip CRLF
strip = re.compile(r'[\r\n]*$')
#ipsecconf = map(lambda x: strip.sub('', x), ipsecconf)
ipsecconf = [strip.sub('', line) for line in ipsecconf]
# Strip comments
strip = re.compile(r'#.*$')
#ipsecconf = map(lambda x: strip.sub('', x), ipsecconf)
ipsecconf = [strip.sub('', line) for line in ipsecconf]
#print ipsecconf
# Look for route
route = ipsec_parse(ipsecconf, connname, 'rightsubnet')
#vip = ipsec_parse(ipsecconf, connname, 'leftsubnet')
if verbose:
print greenraspy, "Info for conn %s: route %s" % (connname, route)
return route
def call_debug_cmd(cmd):
"Call external binary and return output. (Used for debugging.)"
fdes = os.popen(cmd);
output = my_read(fdes)
err = fdes.close()
print output
# IPsec (free/openswan) connection
class IPsecConn:
"""Class that abstracts the free/openswan way of making
IPsec connections, an object of this type represents
one IPsec tunnel."""
def __init__(self, config, section):
"""Setup data structures from config dict, we don't access
the cfg global variable"""
self.name = section
self.route = extract_route(section)
self.oldroute = None
if config.has_option(section, 'route'):
rte = config.get(section, 'route')
if not self.route:
self.route = rte
elif rte != self.route:
self.oldroute = self.route
self.route = rte
if not self.route:
print redraspy, 'Connection %s has no route' % self.name
sys.exit(1)
if config.has_option(section, 'mtu'):
self.mtu = int(config.get(section, 'mtu'))
else:
self.mtu = 1428
if config.has_option(section, 'pinghost'):
self.pinghost = config.get(section, 'pinghost')
self.pingtimeout = int(config.get('global', 'pingtimeout'))
self.pingstatus = None
else:
self.pinghost = ''
self.pingtimeout = 0
self.pingstatus = 'not configured'
self.state = 'unknown'
self.connstarted = 0
self.connstopped = 0
if config.has_option('global', 'virtip'):
self.src = 'src %s' % config.get('global', 'virtip')
else:
self.src = ''
self.netdev = config.get('global', 'netdev')
self.gateway = config.get('global', 'gateway')
self.ipseccmd = config.get('global', 'ipseccmd')
self.ipcmd = config.get('global', 'ipcmd')
if config.has_option(section, 'desc'):
self.desc = "%s(%s)" % (section, config.get(section, 'desc'))
else:
self.desc = "%s(%s)" % (section, self.route)
# Check for unknown options!
for opt in cfg.options(section):
if not opt in conn_kwds:
print yellowraspy, "Unknown option %s in section %s" % (opt, section)
def ipsec_up(self):
"up this ipsec connection"
global outvfn
fdes = os.popen("%s --up %s" % \
(self.ipseccmd, self.name))
txt, tout = read_until(fdes.fileno(),
("IPsec SA established",), 16, outvfn)
if tout < 0:
if verbose:
print redraspy, 'Connection %s failed' % self.desc
return 1
fdes.close()
#time.sleep(1)
return 0
def ipsec_down_unroute(self, what):
"down or unroute (specified by what) ipsec connection"
fdes = os.popen("%s --%s %s" % \
(self.ipseccmd, what, self.name))
txt = my_read(fdes)
if fdes.close():
print txt
def route_up(self, what):
"add/replace (specified by what) route for connection"
#if not self.src:
# return 0
rte = self.route
if rte == 'AUTO':
return 0
fdes = os.popen("%s route %s %s %s dev %s %s mtu %i" % \
(self.ipcmd, what,
rte, self.gateway, self.netdev, self.src, self.mtu))
txt = my_read(fdes)
if fdes.close():
if what == 'add' or what == 'replace' or verbose:
print yellowraspy, '%s route for conn %s failed' % \
(what, self.desc)
print " %s route %s %s %s dev %s %s mtu %i" % \
(self.ipcmd, what, rte, self.gateway,
self.netdev, self.src, self.mtu)
print txt
return 1
return 0
def route_down(self, route = None):
"delete route for connection"
#if not self.src:
# return 0
if not route:
route = self.route
if route == 'AUTO':
return 0
fdes = os.popen("%s route delete %s dev %s" % \
(self.ipcmd, route, self.netdev))
txt = my_read(fdes)
#if verbose:
# print txt
return 0
def route_status(self):
"Check routing table for entry with this connection"
if self.route == 'AUTO':
return 0
fdes = os.popen("%s route show %s" % \
(self.ipcmd, self.route))
txt = my_read(fdes)
if txt and not quiet:
print txt,
fdes.close()
if not txt:
return 3
return 0
def ipsec_status(self):
"Return status of ipsec connection"
fdes = os.popen("%s --status" % self.ipseccmd)
txt = fdes.readlines()
if fdes.close():
print redraspy, 'Could not check connection'
return 4
rgx = re.compile(r'"%s" esp' % self.name)
for line in txt:
if rgx.search(line):
print line,
self.state = 'up'
return 0
return 3
def doping(self):
"""ping with forced timeout (no -w option for ping)
returns 0 on success, anything else on failure"""
import subprocess
process = subprocess.Popen(["/bin/ping", "-c", "1", "%s" % self.pinghost],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
time.sleep(0.1)
rc = process.poll()
if rc != None:
return rc
for iter in range(0, self.pingtimeout):
time.sleep(1)
rc = process.poll()
if rc != None:
return rc
os.kill(process.pid, signal.SIGTERM)
return process.wait()
def ping(self):
"""ping pinghost, ret -1 if not configured,
0 if failed, 1 is successful"""
if self.pinghost:
if self.doping() != 0:
if not quiet:
print yellowraspy, 'ping %s failed %s' % \
(self.pinghost, time.strftime(timefmt))
self.pingstatus = 'failed'
return 0
else:
self.pingstatus = 'succeeded'
return 1
else:
return -1
def up(self):
"""Bring connection up and add route, return 0 for success,
tear down again if not entirely successful."""
ret = self.ipsec_up()
if ret:
return ret
action = 'replace'
#if self.oldroute and not self.route == 'AUTO':
if self.route != 'AUTO':
#call_debug_cmd('%s route show' % self.ipcmd)
#call_debug_cmd('%s addr show' % self.ipcmd)
if self.oldroute:
ret = self.route_down(self.oldroute)
else:
ret = self.route_down(self.route)
action = 'add'
ret = self.route_up(action)
# if 0 and ret:
if ret:
self.ipsec_down_unroute('down')
self.ipsec_down_unroute('unroute')
return ret
self.state = 'up'
self.connstarted = time.time()
# return 0 ## ret
return ret
def down(self):
"Bring connection down, delete route, no return code"
self.ipsec_down_unroute('down')
self.route_down()
self.ipsec_down_unroute('unroute')
self.state = 'down'
self.connstopped = time.time()
def status(self):
"Check for conn status, return 0 for success"
if not quiet:
print greenraspy, 'Conn status %s: %s,' % \
(self.desc, self.state),
if self.connstarted:
print "started %s\n\t" % \
time.strftime(timefmt, time.localtime(self.connstarted)),
if self.connstopped:
print "stopped %s\n\t" % \
time.strftime(timefmt, time.localtime(self.connstopped)),
print "ping: %s" % self.pingstatus
self.route_status()
return self.ipsec_status()
# Global functions again, right encapsulation pending ...
def add_del_ip(what):
"Add/Delete (specified by what) virtual IP address to net interface"
if cfg.has_option('global', 'virtip'):
ipaddr = cfg.get('global', 'virtip')
else:
return
netdev = cfg.get('global', 'netdev')
fdes = os.popen("%s addr %s %s dev %s" % \
(cfg.get('global', 'ipcmd'), what, ipaddr, netdev))
txt = my_read(fdes)
if fdes.close() and not quiet:
print yellowraspy, '%s addr %s %s dev %s failed' % \
(cfg.get('global', 'ipcmd'), what, ipaddr, netdev)
print txt
def setup_connections(config):
"Make and return a list of connections from config sections"
conns = []
for conn in cfg.sections():
if conn == 'global':
continue
conns.append(IPsecConn(config, conn))
return conns
def check_start_ipsec(conn):
"Check if IPsec is running, try to start if not, bail out this fails"
rgx = re.compile(r'"%s"' % conn.name)
outfd = os.popen('%s --status' % cfg.get('global', 'ipseccmd'), 'r')
output = my_read(outfd)
err = outfd.close()
connknown = rgx.search(output)
if err or not connknown:
if err:
sta = 'start'
if not quiet:
print greenraspy, 'Start IPsec'
else:
sta = 'restart'
if not quiet:
print greenraspy, 'Restart IPsec'
# TODO: Disconnect from controlling terminal
outfd = os.popen('%s %s' % (cfg.get('global', 'ipsecinit'), sta), 'r')
output = my_read(outfd)
err = outfd.close()
if err:
print redraspy, 'Fatal: Could not start IPsec'
print output
sys.exit(7)
my_sleep(5)
if verbose:
print greenraspy, 'IPsec running'
def up_connections(conns):
"Start all IPsec connections from list conns"
check_start_ipsec(conns[0])
add_del_ip('add')
upconns = []
for conn in conns:
if not quiet:
print greenraspy, 'start ipsec connection %s' % conn.desc
ret = conn.up()
if ret or interrupted:
if not quiet:
print redraspy, 'Connection %s failed, tear down' % conn.desc
down_connections(upconns)
add_del_ip('del')
return 1
else:
upconns.append(conn)
if not quiet:
print greenraspy, 'ipsec connections up %s' % \
time.strftime(timefmt)
return 0
def do_pings(conns):
"Ping all connections (in conns), returns 1 if _all_ fail"
succ = 0
pings = 0
ival = int(cfg.get('global', 'pinginterval'))
for conn in conns:
if reconnect or interrupted:
return 0
ret = conn.ping()
if ret == -1:
# Not configured, try next connection
continue
else:
# Count successful pings
succ += ret
# Count tried pings
pings += 1
my_sleep(ival)
if not pings:
# If no ping has been tried, we need to sleep anyway
my_sleep(ival)
return 0
if not succ:
# We have tried some pings, but none succeeded
return pings
return 0
def down_connections(conns):
"Stop all IPsec connections in conns"
for conn in conns:
if not quiet:
print greenraspy, 'stop ipsec connection %s' % conn.desc
conn.down()
add_del_ip('del')
if not quiet:
print greenraspy, 'ipsec connections stopped %s' % \
time.strftime(timefmt)
def up_all(conns):
"""Up all connections in conns. This is the main loop for
ras.py script."""
global interrupted, reconnect, connections, alreadyauth
notconn = 0
# We need it anyway, even if we are connected already
if cfg.has_option('global', 'authhost'):
input_passphrase()
start = time.time()
if daemon:
daemonize()
if not quiet:
print green + "ras.py started %s" % time.strftime(timefmt) + norm
while not interrupted and ( \
not cfg.has_option('global', 'maxconnect') \
or time.time() < start + int(cfg.get('global', 'maxconnect'))):
# Skip auth session if tunnels exist already
if notconn or conns[0].status():
if cfg.has_option('global', 'authhost'):
if alreadyauth == 0 and auth_session():
break
else:
alreadyauth = 0
if up_connections(conns):
interrupted = 1
break
else:
if not quiet:
print greenraspy, 'Connections already up'
# Ping loop
err = 0
while err < int(cfg.get('global', 'pingfail')) \
and not interrupted and not reconnect:
ret = do_pings(conns)
if ret:
err += ret
else:
err = 0
# Pings failed
down_connections(conns)
notconn = 1
if reconnect:
read_config(configfile, 1)
conns = setup_connections(cfg)
# FIXME: No point in passing conns here if we access the global
connections = conns
os.utime(lockfilename, None)
reconnect = 0
my_sleep(int(cfg.get('global', 'failholdoff')))
if not quiet:
print green + "ras.py stopped %s" % time.strftime(timefmt) + norm
if daemon:
del_lockfile()
return interrupted
def down_all(conns):
"Down all connections (from list conns)"
# We could add logic here to kill the right PID ...
down_connections(conns)
return 0
def status_all(conns):
"Query all connection objects (in conns) for status"
ret = 0
for conn in conns:
retconn = conn.status()
if retconn:
ret = retconn
if not quiet:
if ret:
print redraspy, 'down'
else:
print greenraspy, 'up'
return ret
def print_logfile(beginmarker, endmarker = '^@$%&#', beginmarker2 = '^@$%&#'):
"""Outputs the section from the logfile since the last
beginmarker, avoiding endmarker."""
logf = open(logfilename)
logl = logf.readlines()
endlog = ''
for line in logl:
if beginmarker in line:
endlog = ''
elif beginmarker2 in line:
endlog = ''
elif endmarker in line:
pass
else:
endlog += line
print endlog
logf.close()
return endlog
def file_newer(file1, file2):
"Returns a number > 0 if file1 is newer (mtime) than file2 else <= 0"
stat1 = os.stat(file1)
stat2 = os.stat(file2)
return stat1.st_mtime - stat2.st_mtime
def usage():
"Tell the user how to use this script"
print bold + "Usage: ras.py [options] action [cfg file]" + norm
print bold + " options: -v (verbose), -q (quiet), \n\t-d (daemonize)," \
+ " -D (Direct) -a (alreadyauthenticated)"
print " action: (start|up|status|help|stop|down|"
print "\t try-restart|condrestart|restart|reload|force-reload|probe)" \
+ norm
return 2
def print_help():
"Description of what this script does and how it's configured"
print __revision__
print bold + "This script connects you to an IPsec RAS service.\n" + norm
print "Assumptions: The FreeS/WAN or openswan /etc/ipsec.conf and"
print "/etc/ipsec.secrets config files are configured correctly and"
print "contain the descriptions of the connections and the needed secrets."
print "The connections should be configured to be added, NOT started"
print "on /etc/init.d/ipsec start, i.e. auto=add in ipsec.conf."
print "The script assumes that we are the left side of the connection."
print " Note: It might work for people with auto=start, but I haven't"
print " tested this. Nor have I tested without private virtual IP.\n"
print "This script should be executed setuid root."
print "It is configured by /etc/ras.config (or another cfg file specified)"
print "which has the .ini file syntax as described below.\n"
print "The [global] section has the following keys (all optional!):"
print " pinginterval: How many seconds to wait between pings (default: 20)."
print " pingtimeout: How long (s) to wait for our peer to answer (default: 4)."
print " pingfail: How many subsequent pings need to fail to make the script"
print "\tassume that the connectivity is down (default: 4)."
print " failholdoff: How long to sleep before trying to reconnect (def: 150)."
print " maxconnect: How many seconds we may be connected in total"
print "\t(default: empty => infinite)."
print " virtip: virtual IP of your end of the IPsec connection "
print "\t(default: empty => no virtual IP is used)."
print " netdev: name of the NIC that's used to send out IPsec traffic"
print "\t(default: empty => ipsec0 for KLIPS, netdev of defaultroute otherwise)."
print " gateway: set to an empty string or 'via IP' to explicitly set routing GW"
print "\t(default: if no netdev is specified take defroute gw, otherwise '')."
print " authhost: Machine to telnet to for the opie auth session"
print "\t(default: none => skip opie auth session)."
print " authretry: How many times to try auth session if telnet times out."
print "\t0 means infinity (default: 3)."
print " logname: username to log in as into authhost"
print "\t(default: user running the script).\n"
print "Expert options are:"
print " ipseccmd: Let's you override the command to control ipsec tunnels"
print "\twith --up/--down/--status conn.name (default: /usr/sbin/ipsec auto)"
print " ipsecinit: Let's you the command to call the ipsec init script"
print "\t(default: /etc/init.d/ipsec)"
print " ipsecconfread: Let's you override how to read FreeSWAN's ipsec.conf"
print "\t(default: cat /etc/ipsec.conf)"
print " ipcmd: The iproute2 command (default: /sbin/ip)"
print " routeraw: How to get raw routing table (default: cat /proc/net/route)"
print " netdevexist: How to determine existence of a network device"
print "\t(default: test -e /proc/sys/net/ipv4/conf/)"
print "The most common use for these expoert options is to prefix commands with"
print "ssh -t to remote control another machine who handles the tunnels. This"
print "can be more easily achieved by specifying"
print " cmdprefix: Let's you prefix the commands ipseccmd, ipsecinit,"
print "\tipsecconfread, ipcmd, routeraw, and netdevexist."
print "\tNote that individual settings are not prefixed automatically.\n"
print "The global section is mandatory, but it can be empty if the"
print "defaults all work for you.\n"
print "There are additional sections where the section name does specify"
print "the name of the IPsec connection as given in ipsec.conf."
print "The following entries per section are supported:"
print "route: The route that should be set, should be equal to the"
print "\tother side's subnet, e.g. 10.0.0.0/8 (optional)."
print "\tThe route is taken from rightsubnet in ipsec.conf, but can be"
print "\toverriden here."
print "\tThe special word 'AUTO' here prevents ras.py to affect your routes"
print "\tbut assumes that the ipcseccmd has taken care of it."
print "pinghost: Machine to periodically send ping commands to (optional).\n"
print "mtu: MTU for the route (optional, default 1428).\n"
print "desc: Description of the connection (optional).\n"
print "Multiple connections can be specified this way.\n"
print "The script is verbose about success or failure, the return codes"
print "roughly match the ones from LSB init scripts.\n"
print "The program can be run in daemon mode, in which case it forks"
print "a background process; subsequent calls of this program will connect"
print "to the daemon by sending it signals."
print "The signals SIGTERM/SIGINT/SIGQUIT terminate the process/daemon,"
print "the signal SIGUSR1 makes it log status information, and"
print "the signal SIGUSR2 makes it close the connections, reread the config"
print "file and reconnect. SIGHUP reacts like SIGUSR2 in daemon mode, and"
print "like SIGTERM in foreground mode.\n"
def sighandler(sig, frame):
"Signal handler invoked to down connections and stop the script"
global interrupted
#global yellowraspy, quiet
interrupted = sig + 128
if not quiet:
print yellowraspy, 'Signal %i caught %s, bailing out' % (sig, frame)
def sighandler_status(sig, frame):
"Signal handler to create status log entry"
#global connections, BEGINRASSTATUS, ENDRASSTATUS
print BEGINRASSTATUS
status_all(connections)
print ENDRASSTATUS
def sighandler_reconnect(sig, frame):
"Signal handler to reconnect connections"
global reconnect
#global yellowraspy, quiet
reconnect = 1
if not quiet:
print yellowraspy, 'Signal %i caught %s, reconnecting' % (sig, frame)
def setup_signal_handlers():
"setup signal handlers"
signal.signal(signal.SIGTERM, sighandler)
signal.signal(signal.SIGINT , sighandler)
signal.signal(signal.SIGQUIT, sighandler)
signal.signal(signal.SIGHUP , sighandler) # override in daemon mode
signal.signal(signal.SIGUSR1, sighandler_status)
signal.signal(signal.SIGUSR2, sighandler_reconnect)
def parse_args(argv):
"Parse command line args"
import getopt
global daemon, quiet, verbose, outqfn, outvfn, alreadyauth, configfile
# daemon running?
lpid = check_lockfile()
# options
try:
optlist, args = getopt.gnu_getopt(argv, 'vqdaDh', ('help',))
except getopt.GetoptError, exc:
print exc
sys.exit(usage())
for opt in optlist:
if opt[0] == '-q':
quiet = 1
outqfn = None
continue
if opt[0] == '-v':
verbose = 1
quiet = 0
outqfn = sys.stdout.fileno()
outvfn = sys.stdout.fileno()
continue
if opt[0] == '-d':
daemon = 1
continue
if opt[0] == '-h' or opt[0] == '--help':
print_help()
sys.exit(usage())
#continue
if opt[0] == '-a':
alreadyauth = 1 # override
continue
if opt[0] == '-D':
lpid = 0 # override
continue
if len(args) < 2 or len(args) > 3:
sys.exit(usage())
# Help
if args[1] == 'help':
print_help()
sys.exit(usage())
if len(args) > 2:
configfile = args[2]
return args[1], lpid
def do_control_daemon(action, lpid):
"Frontend to control ras.py that's running in daemon mode"
global daemon
if action in ('start', 'up'):
print redraspy, "ras.py already running (pid %i)" % lpid
return 0
elif action == 'status':
try:
os.kill(lpid, signal.SIGUSR1)
except OSError, err:
print yellowraspy, str(err)
return status_all(connections)
my_sleep(1)
log = print_logfile(BEGINRASSTATUS, ENDRASSTATUS, NEWRASSESSION)
if (greenraspy + ' up') in log:
return 0
else:
return 1
elif action in ('stop', 'down'):
try:
os.kill(lpid, signal.SIGTERM)
except OSError, err:
print redraspy, str(err)
return down_all(connections)
my_sleep(1)
print_logfile(NEWRASSESSION)
return 0
elif action in ('restart', 'try-restart', 'condrestart'):
try:
os.kill(lpid, signal.SIGTERM)
except OSError, err:
print redraspy, str(err)
return 4
my_sleep(1)
daemon = 1
return do_direct_operation('start')
elif action in ('reload', 'force-reload'):
try:
os.kill(lpid, signal.SIGHUP)
except OSError, err:
print redraspy, str(err)
return 4
return 0
elif action == 'probe':
if file_newer(configfile, lockfilename) > 0:
print 'reload'
return 0
else:
return 1
else:
return -1
def do_direct_operation(action):
"start, stop, status in foreground mode"
if action in ('start', 'up', 'restart', 'try-restart', 'condrestart'):
# FIXME: This is fuzzy ...
if os.getuid() != 0 and not cfg.has_option('global', 'cmdprefix'):
print redraspy, 'Need to be root'
return 4
else:
if action in ('try-restart', 'condrestart', 'restart'):
ret = status_all(connections)
if not ret:
print redraspy, \
"Can't stop running ras.py that's not in daemon mode"
return 4
elif action != 'restart':
return 0
else:
down_all(connections)
return up_all(connections)
elif action == 'status':
return status_all(connections)
elif action in ('stop', 'down'):
return down_all(connections)
elif action in ('reload', 'force-reload', 'probe'):
print redraspy, 'Not running as daemon'
return 7
else:
return -1
# main routine
def main(argv):
"Main program ras.py"
#global daemon, quiet, verbose, outqfn, outvfn
global connections, cfg, configfile
sanitize_env()
setup_signal_handlers()
action, lpid = parse_args(argv)
# connections
check_whiterussian()
read_config(configfile)
check_prereq()
connections = setup_connections(cfg)
# action
if lpid:
ret = do_control_daemon(action, lpid)
else:
ret = do_direct_operation(action)
if ret < 0:
return usage()
else:
return ret
# run main if called standalone
if __name__ == "__main__":
sys.exit(main(sys.argv))