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