File ras.py of Package ras

#!/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))