diff options
Diffstat (limited to 'src/leap/eip/openvpnconnection.py')
-rw-r--r-- | src/leap/eip/openvpnconnection.py | 410 |
1 files changed, 410 insertions, 0 deletions
diff --git a/src/leap/eip/openvpnconnection.py b/src/leap/eip/openvpnconnection.py new file mode 100644 index 00000000..bee8c010 --- /dev/null +++ b/src/leap/eip/openvpnconnection.py @@ -0,0 +1,410 @@ +""" +OpenVPN Connection +""" +from __future__ import (print_function) +from functools import partial +import logging +import os +import psutil +import shutil +import select +import socket +from time import sleep + +logger = logging.getLogger(name=__name__) + +from leap.base.connection import Connection +from leap.base.constants import OPENVPN_BIN +from leap.util.coroutines import spawn_and_watch_process +from leap.util.misc import get_openvpn_pids + +from leap.eip.udstelnet import UDSTelnet +from leap.eip import config as eip_config +from leap.eip import exceptions as eip_exceptions + + +class OpenVPNManagement(object): + + # TODO explain a little bit how management interface works + # and our telnet interface with support for unix sockets. + + """ + for more information, read openvpn management notes. + zcat `dpkg -L openvpn | grep management` + """ + + def _connect_to_management(self): + """ + Connect to openvpn management interface + """ + if hasattr(self, 'tn'): + self._close_management_socket() + self.tn = UDSTelnet(self.host, self.port) + + # XXX make password optional + # specially for win. we should generate + # the pass on the fly when invoking manager + # from conductor + + #self.tn.read_until('ENTER PASSWORD:', 2) + #self.tn.write(self.password + '\n') + #self.tn.read_until('SUCCESS:', 2) + if self.tn: + self._seek_to_eof() + return True + + def _close_management_socket(self, announce=True): + """ + Close connection to openvpn management interface + """ + logger.debug('closing socket') + if announce: + self.tn.write("quit\n") + self.tn.read_all() + self.tn.get_socket().close() + del self.tn + + def _seek_to_eof(self): + """ + Read as much as available. Position seek pointer to end of stream + """ + try: + b = self.tn.read_eager() + except EOFError: + logger.debug("Could not read from socket. Assuming it died.") + return + while b: + try: + b = self.tn.read_eager() + except EOFError: + logger.debug("Could not read from socket. Assuming it died.") + + def _send_command(self, cmd): + """ + Send a command to openvpn and return response as list + """ + if not self.connected(): + try: + self._connect_to_management() + except eip_exceptions.MissingSocketError: + #logger.warning('missing management socket') + return [] + try: + if hasattr(self, 'tn'): + self.tn.write(cmd + "\n") + except socket.error: + logger.error('socket error') + self._close_management_socket(announce=False) + return [] + try: + buf = self.tn.read_until(b"END", 2) + self._seek_to_eof() + blist = buf.split('\r\n') + if blist[-1].startswith('END'): + del blist[-1] + return blist + else: + return [] + except socket.error as exc: + logger.debug('socket error: %s' % exc.message) + except select.error as exc: + logger.debug('select error: %s' % exc.message) + + def _send_short_command(self, cmd): + """ + parse output from commands that are + delimited by "success" instead + """ + if not self.connected(): + self.connect() + self.tn.write(cmd + "\n") + # XXX not working? + buf = self.tn.read_until(b"SUCCESS", 2) + self._seek_to_eof() + blist = buf.split('\r\n') + return blist + + # + # random maybe useful vpn commands + # + + def pid(self): + #XXX broken + return self._send_short_command("pid") + + +class OpenVPNConnection(Connection, OpenVPNManagement): + """ + All related to invocation + of the openvpn binary. + It's extended by EIPConnection. + """ + + # XXX Inheriting from Connection was an early design idea + # but currently that's an empty class. + # We can get rid of that if we don't use it for sharing + # state with other leap modules. + + def __init__(self, + watcher_cb=None, + debug=False, + host=None, + port="unix", + password=None, + *args, **kwargs): + """ + :param watcher_cb: callback to be \ +called for each line in watched stdout + :param signal_map: dictionary of signal names and callables \ +to be triggered for each one of them. + :type watcher_cb: function + :type signal_map: dict + """ + #XXX FIXME + #change watcher_cb to line_observer + # XXX if not host: raise ImproperlyConfigured + + logger.debug('init openvpn connection') + self.debug = debug + self.ovpn_verbosity = kwargs.get('ovpn_verbosity', None) + + self.watcher_cb = watcher_cb + #self.signal_maps = signal_maps + + self.subp = None + self.watcher = None + + self.server = None + self.port = None + self.proto = None + + self.command = None + self.args = None + + # XXX get autostart from config + self.autostart = True + + # management interface init + self.host = host + if isinstance(port, str) and port.isdigit(): + port = int(port) + elif port == "unix": + port = "unix" + else: + port = None + self.port = port + self.password = password + + def run_openvpn_checks(self): + """ + runs check needed before launching + openvpn subprocess. will raise if errors found. + """ + logger.debug('running openvpn checks') + # XXX I think that "check_if_running" should be called + # from try openvpn connection instead. -- kali. + # let's prepare tests for that before changing it... + self._check_if_running_instance() + self._set_ovpn_command() + self._check_vpn_keys() + + def try_openvpn_connection(self): + """ + attempts to connect + """ + # XXX should make public method + if self.command is None: + raise eip_exceptions.EIPNoCommandError + if self.subp is not None: + logger.debug('cowardly refusing to launch subprocess again') + # XXX this is not returning ???!! + # FIXME -- so it's calling it all the same!! + + self._launch_openvpn() + + def connected(self): + """ + Returns True if connected + rtype: bool + """ + # XXX make a property + return hasattr(self, 'tn') + + def terminate_openvpn_connection(self, shutdown=False): + """ + terminates openvpn child subprocess + """ + if self.subp: + try: + self._stop_openvpn() + except eip_exceptions.ConnectionRefusedError: + logger.warning( + 'unable to send sigterm signal to openvpn: ' + 'connection refused.') + + # XXX kali -- + # XXX review-me + # I think this will block if child process + # does not return. + # Maybe we can .poll() for a given + # interval and exit in any case. + + RETCODE = self.subp.wait() + if RETCODE: + logger.error( + 'cannot terminate subprocess! Retcode %s' + '(We might have left openvpn running)' % RETCODE) + + if shutdown: + self._cleanup_tempfiles() + + def _cleanup_tempfiles(self): + """ + remove all temporal files + we might have left behind + """ + # if self.port is 'unix', we have + # created a temporal socket path that, under + # normal circumstances, we should be able to + # delete + + if self.port == "unix": + logger.debug('cleaning socket file temp folder') + + tempfolder = os.path.split(self.host)[0] + if os.path.isdir(tempfolder): + try: + shutil.rmtree(tempfolder) + except OSError: + logger.error('could not delete tmpfolder %s' % tempfolder) + + # checks + + def _check_if_running_instance(self): + """ + check if openvpn is already running + """ + openvpn_pids = get_openvpn_pids() + if openvpn_pids: + logger.debug('an openvpn instance is already running.') + logger.debug('attempting to stop openvpn instance.') + if not self._stop_openvpn(): + raise eip_exceptions.OpenVPNAlreadyRunning + return + else: + logger.debug('no openvpn instance found.') + + def _set_ovpn_command(self): + try: + command, args = eip_config.build_ovpn_command( + provider=self.provider, + debug=self.debug, + socket_path=self.host, + ovpn_verbosity=self.ovpn_verbosity) + except eip_exceptions.EIPNoPolkitAuthAgentAvailable: + command = args = None + raise + except eip_exceptions.EIPNoPkexecAvailable: + command = args = None + raise + + # XXX if not command, signal error. + self.command = command + self.args = args + + def _check_vpn_keys(self): + """ + checks for correct permissions on vpn keys + """ + try: + eip_config.check_vpn_keys(provider=self.provider) + except eip_exceptions.EIPInitBadKeyFilePermError: + logger.error('Bad VPN Keys permission!') + # do nothing now + # and raise the rest ... + + # starting and stopping openvpn subprocess + + def _launch_openvpn(self): + """ + invocation of openvpn binaries in a subprocess. + """ + #XXX TODO: + #deprecate watcher_cb, + #use _only_ signal_maps instead + + #logger.debug('_launch_openvpn called') + if self.watcher_cb is not None: + linewrite_callback = self.watcher_cb + else: + #XXX get logger instead + linewrite_callback = lambda line: logger.debug( + 'watcher: %s' % line) + + # the partial is not + # being applied now because we're not observing the process + # stdout like we did in the early stages. but I leave it + # here since it will be handy for observing patterns in the + # thru-the-manager updates (with regex) + observers = (linewrite_callback, + partial(lambda con_status, + line: linewrite_callback, self.status)) + subp, watcher = spawn_and_watch_process( + self.command, + self.args, + observers=observers) + self.subp = subp + self.watcher = watcher + + def _stop_openvpn(self): + """ + stop openvpn process + by sending SIGTERM to the management + interface + """ + # XXX method a bit too long, split + logger.debug("atempting to terminate openvpn process...") + if self.connected(): + try: + self._send_command("signal SIGTERM\n") + sleep(1) + if not self.subp: # XXX ??? + return True + except socket.error: + logger.warning('management socket died') + return + + #shutting openvpn failured + #try patching in old openvpn host and trying again + # XXX could be more than one! + process = self._get_openvpn_process() + if process: + logger.debug('process: %s' % process.name) + cmdline = process.cmdline + + manag_flag = "--management" + if isinstance(cmdline, list) and manag_flag in cmdline: + _index = cmdline.index(manag_flag) + self.host = cmdline[_index + 1] + self._send_command("signal SIGTERM\n") + + #make sure the process was terminated + process = self._get_openvpn_process() + if not process: + logger.debug("Existing OpenVPN Process Terminated") + return True + else: + logger.error("Unable to terminate existing OpenVPN Process.") + return False + + return True + + def _get_openvpn_process(self): + for process in psutil.process_iter(): + if OPENVPN_BIN in process.name: + return process + return None + + def get_log(self, lines=1): + log = self._send_command("log %s" % lines) + return log |