import os import shutil import socket from twisted.internet import defer, reactor from twisted.logger import Logger import psutil try: # psutil < 2.0.0 from psutil.error import AccessDenied as psutil_AccessDenied PSUTIL_2 = False except ImportError: # psutil >= 2.0.0 from psutil import AccessDenied as psutil_AccessDenied PSUTIL_2 = True from ._telnet import UDSTelnet logger = Logger() class OpenVPNAlreadyRunning(Exception): message = ("Another openvpn instance is already running, and could " "not be stopped.") class AlienOpenVPNAlreadyRunning(Exception): message = ("Another openvpn instance is already running, and could " "not be stopped because it was not launched by LEAP.") class VPNManagement(object): """ This is a mixin that we use in the VPNProcess class. Here we get together all methods related with the openvpn management interface. For more info about management methods:: zcat `dpkg -L openvpn | grep management` """ # Timers, in secs CONNECTION_RETRY_TIME = 1 def __init__(self, signaler=None): """ Initializes the VPNManager. :param signaler: Signaler object used to send notifications to the backend :type signaler: backend.Signaler """ self._tn = None self._signaler = signaler self.aborted = False def _seek_to_eof(self): """ Read as much as available. Position seek pointer to end of stream """ try: self._tn.read_eager() except EOFError: logger.debug("Could not read from socket. Assuming it died.") return def _send_command(self, command, until=b"END"): """ Sends a command to the telnet connection and reads until END is reached. :param command: command to send :type command: str :param until: byte delimiter string for reading command output :type until: byte str :return: response read :rtype: list """ # leap_assert(self._tn, "We need a tn connection!") try: self._tn.write("%s\n" % (command,)) buf = self._tn.read_until(until, 2) self._seek_to_eof() blist = buf.split('\r\n') if blist[-1].startswith(until): del blist[-1] return blist else: return [] except socket.error: # XXX should get a counter and repeat only # after mod X times. logger.warn('socket error (command was: "%s")' % (command,)) self._close_management_socket(announce=False) logger.debug('trying to connect to management again') self.try_to_connect_to_management(max_retries=5) return [] # XXX should move this to a errBack! except Exception as e: logger.warn("Error sending command %s: %r" % (command, e)) return [] 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() self._tn = None def _connect_management(self, socket_host, socket_port): """ Connects to the management interface on the specified socket_host socket_port. :param socket_host: either socket path (unix) or socket IP :type socket_host: str :param socket_port: either string "unix" if it's a unix socket, or port otherwise :type socket_port: str """ if self.is_connected(): self._close_management_socket() try: self._tn = UDSTelnet(socket_host, socket_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._tn.read_eager() # XXX move this to the Errback except Exception as e: logger.warn("Could not connect to OpenVPN yet: %r" % (e,)) self._tn = None def _connectCb(self, *args): """ Callback for connection. :param args: not used """ if self._tn: logger.info('Connected to management') else: logger.debug('Cannot connect to management...') def _connectErr(self, failure): """ Errorback for connection. :param failure: Failure """ logger.warn(failure) def connect_to_management(self, host, port): """ Connect to a management interface. :param host: the host of the management interface :type host: str :param port: the port of the management interface :type port: str :returns: a deferred """ self.connectd = defer.maybeDeferred( self._connect_management, host, port) self.connectd.addCallbacks(self._connectCb, self._connectErr) return self.connectd def is_connected(self): """ Returns the status of the management interface. :returns: True if connected, False otherwise :rtype: bool """ return True if self._tn else False def try_to_connect_to_management(self, retry=0, max_retries=None): """ Attempts to connect to a management interface, and retries after CONNECTION_RETRY_TIME if not successful. :param retry: number of the retry :type retry: int """ if max_retries and retry > max_retries: logger.warn("Max retries reached while attempting to connect " "to management. Aborting.") self.aborted = True return # _alive flag is set in the VPNProcess class. if not self._alive: logger.debug('Tried to connect to management but process is ' 'not alive.') return logger.debug('trying to connect to management') if not self.aborted and not self.is_connected(): self.connect_to_management(self._socket_host, self._socket_port) reactor.callLater( self.CONNECTION_RETRY_TIME, self.try_to_connect_to_management, retry + 1) def _parse_state_and_notify(self, output): """ Parses the output of the state command and emits state_changed signal when the state changes. :param output: list of lines that the state command printed as its output :type output: list """ for line in output: stripped = line.strip() if stripped == "END": continue parts = stripped.split(",") if len(parts) < 5: continue ts, status_step, ok, ip, remote = parts state = status_step if state != self._last_state: if self._signaler is not None: self._signaler.signal( self._signaler.eip_state_changed, state) self._last_state = state def _parse_status_and_notify(self, output): """ Parses the output of the status command and emits status_changed signal when the status changes. :param output: list of lines that the status command printed as its output :type output: list """ tun_tap_read = "" tun_tap_write = "" for line in output: stripped = line.strip() if stripped.endswith("STATISTICS") or stripped == "END": continue parts = stripped.split(",") if len(parts) < 2: continue text, value = parts # text can be: # "TUN/TAP read bytes" # "TUN/TAP write bytes" # "TCP/UDP read bytes" # "TCP/UDP write bytes" # "Auth read bytes" if text == "TUN/TAP read bytes": tun_tap_read = value # download elif text == "TUN/TAP write bytes": tun_tap_write = value # upload status = (tun_tap_read, tun_tap_write) if status != self._last_status: if self._signaler is not None: self._signaler.signal( self._signaler.eip_status_changed, status) self._last_status = status def get_state(self): """ Notifies the gui of the output of the state command over the openvpn management interface. """ if self.is_connected(): return self._parse_state_and_notify(self._send_command("state")) def get_status(self): """ Notifies the gui of the output of the status command over the openvpn management interface. """ if self.is_connected(): return self._parse_status_and_notify(self._send_command("status")) @property def vpn_env(self): """ Return a dict containing the vpn environment to be used. """ return self._launcher.get_vpn_env() def terminate_openvpn(self, shutdown=False): """ Attempts to terminate openvpn by sending a SIGTERM. """ if self.is_connected(): self._send_command("signal SIGTERM") if shutdown: self._cleanup_tempfiles() def _cleanup_tempfiles(self): """ Remove all temporal files we might have left behind. Iif self.port is 'unix', we have created a temporal socket path that, under normal circumstances, we should be able to delete. """ if self._socket_port == "unix": logger.debug('cleaning socket file temp folder') tempfolder = _first(os.path.split(self._socket_host)) if tempfolder and os.path.isdir(tempfolder): try: shutil.rmtree(tempfolder) except OSError: logger.error('could not delete tmpfolder %s' % tempfolder) def get_openvpn_process(self): """ Looks for openvpn instances running. :rtype: process """ openvpn_process = None for p in psutil.process_iter(): try: # XXX Not exact! # Will give false positives. # we should check that cmdline BEGINS # with openvpn or with our wrapper # (pkexec / osascript / whatever) # This needs more work, see #3268, but for the moment # we need to be able to filter out arguments in the form # --openvpn-foo, since otherwise we are shooting ourselves # in the feet. if PSUTIL_2: cmdline = p.cmdline() else: cmdline = p.cmdline if any(map(lambda s: s.find( "LEAPOPENVPN") != -1, cmdline)): openvpn_process = p break except psutil_AccessDenied: pass return openvpn_process def stop_if_already_running(self): """ Checks if VPN is already running and tries to stop it. Might raise OpenVPNAlreadyRunning. :return: True if stopped, False otherwise """ process = self.get_openvpn_process() if not process: logger.debug('Could not find openvpn process while ' 'trying to stop it.') return logger.debug("OpenVPN is already running, trying to stop it...") cmdline = process.cmdline manag_flag = "--management" if isinstance(cmdline, list) and manag_flag in cmdline: # we know that our invocation has this distinctive fragment, so # we use this fingerprint to tell other invocations apart. # this might break if we change the configuration path in the # launchers def smellslikeleap(s): return "leap" in s and "providers" in s if not any(map(smellslikeleap, cmdline)): logger.debug("We cannot stop this instance since we do not " "recognise it as a leap invocation.") raise AlienOpenVPNAlreadyRunning try: index = cmdline.index(manag_flag) host = cmdline[index + 1] port = cmdline[index + 2] logger.debug("Trying to connect to %s:%s" % (host, port)) self.connect_to_management(host, port) # XXX this has a problem with connections to different # remotes. So the reconnection will only work when we are # terminating instances left running for the same provider. # If we are killing an openvpn instance configured for another # provider, we will get: # TLS Error: local/remote TLS keys are out of sync # However, that should be a rare case right now. self._send_command("signal SIGTERM") self._close_management_socket(announce=True) except (Exception, AssertionError) as e: logger.warn("Problem trying to terminate OpenVPN: %r" % (e,)) else: logger.debug("Could not find the expected openvpn command line.") process = self.get_openvpn_process() if process is None: logger.debug("Successfully finished already running " "openvpn process.") return True else: logger.warn("Unable to terminate OpenVPN") raise OpenVPNAlreadyRunning def _first(things): try: return things[0] except (IndexError, TypeError): return None