summaryrefslogtreecommitdiff
path: root/src/leap/eip/openvpnconnection.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/eip/openvpnconnection.py')
-rw-r--r--src/leap/eip/openvpnconnection.py410
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