diff options
Diffstat (limited to 'src/leap/bitmask/vpn/_control.py')
-rw-r--r-- | src/leap/bitmask/vpn/_control.py | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/src/leap/bitmask/vpn/_control.py b/src/leap/bitmask/vpn/_control.py new file mode 100644 index 0000000..991dc0f --- /dev/null +++ b/src/leap/bitmask/vpn/_control.py @@ -0,0 +1,198 @@ +class VPNControl(object): + """ + This is the high-level object that the service is dealing with. + It exposes the start and terminate methods. + + On start, it spawns a VPNProcess instance that will use a vpnlauncher + suited for the running platform and connect to the management interface + opened by the openvpn process, executing commands over that interface on + demand. + """ + TERMINATE_MAXTRIES = 10 + TERMINATE_WAIT = 1 # secs + + OPENVPN_VERB = "openvpn_verb" + + def __init__(self, **kwargs): + """ + Instantiate empty attributes and get a copy + of a QObject containing the QSignals that we will pass along + to the VPNManager. + """ + self._vpnproc = None + self._pollers = [] + + self._signaler = kwargs['signaler'] + # self._openvpn_verb = flags.OPENVPN_VERBOSITY + self._openvpn_verb = None + + self._user_stopped = False + self._remotes = kwargs['remotes'] + + def start(self, *args, **kwargs): + """ + Starts the openvpn subprocess. + + :param args: args to be passed to the VPNProcess + :type args: tuple + + :param kwargs: kwargs to be passed to the VPNProcess + :type kwargs: dict + """ + logger.debug('VPN: start') + self._user_stopped = False + self._stop_pollers() + kwargs['openvpn_verb'] = self._openvpn_verb + kwargs['signaler'] = self._signaler + kwargs['remotes'] = self._remotes + + # start the main vpn subprocess + vpnproc = VPNProcess(*args, **kwargs) + + if vpnproc.get_openvpn_process(): + logger.info("Another vpn process is running. Will try to stop it.") + vpnproc.stop_if_already_running() + + # FIXME it would be good to document where the + # errors here are catched, since we currently handle them + # at the frontend layer. This *should* move to be handled entirely + # in the backend. + # exception is indeed technically catched in backend, then converted + # into a signal, that is catched in the eip_status widget in the + # frontend, and converted into a signal to abort the connection that is + # sent to the backend again. + + # the whole exception catching should be done in the backend, without + # the ping-pong to the frontend, and without adding any logical checks + # in the frontend. We should just communicate UI changes to frontend, + # and abstract us away from anything else. + try: + cmd = vpnproc.getCommand() + except Exception as e: + logger.error("Error while getting vpn command... {0!r}".format(e)) + raise + + env = os.environ + for key, val in vpnproc.vpn_env.items(): + env[key] = val + + reactor.spawnProcess(vpnproc, cmd[0], cmd, env) + self._vpnproc = vpnproc + + # add pollers for status and state + # this could be extended to a collection of + # generic watchers + + poll_list = [LoopingCall(vpnproc.pollStatus), + LoopingCall(vpnproc.pollState)] + self._pollers.extend(poll_list) + self._start_pollers() + + + # TODO -- rename to stop ?? + def terminate(self, shutdown=False, restart=False): + """ + Stops the openvpn subprocess. + + Attempts to send a SIGTERM first, and after a timeout + it sends a SIGKILL. + + :param shutdown: whether this is the final shutdown + :type shutdown: bool + :param restart: whether this stop is part of a hard restart. + :type restart: bool + """ + self._stop_pollers() + + # First we try to be polite and send a SIGTERM... + if self._vpnproc is not None: + # We assume that the only valid stops are initiated + # by an user action, not hard restarts + self._user_stopped = not restart + self._vpnproc.is_restart = restart + + self._sentterm = True + self._vpnproc.terminate_openvpn(shutdown=shutdown) + + # ...but we also trigger a countdown to be unpolite + # if strictly needed. + reactor.callLater( + self.TERMINATE_WAIT, self._kill_if_left_alive) + else: + logger.debug("VPN is not running.") + + + # TODO should this be public?? + def killit(self): + """ + Sends a kill signal to the process. + """ + self._stop_pollers() + if self._vpnproc is None: + logger.debug("There's no vpn process running to kill.") + else: + self._vpnproc.aborted = True + self._vpnproc.killProcess() + + + def bitmask_root_vpn_down(self): + """ + Bring openvpn down using the privileged wrapper. + """ + if IS_MAC: + # We don't support Mac so far + return True + BM_ROOT = force_eval(linux.LinuxVPNLauncher.BITMASK_ROOT) + + # FIXME -- port to processProtocol + exitCode = subprocess.call(["pkexec", + BM_ROOT, "openvpn", "stop"]) + return True if exitCode is 0 else False + + + def _kill_if_left_alive(self, tries=0): + """ + Check if the process is still alive, and send a + SIGKILL after a timeout period. + + :param tries: counter of tries, used in recursion + :type tries: int + """ + while tries < self.TERMINATE_MAXTRIES: + if self._vpnproc.transport.pid is None: + logger.debug("Process has been happily terminated.") + return + else: + logger.debug("Process did not die, waiting...") + + tries += 1 + reactor.callLater(self.TERMINATE_WAIT, + self._kill_if_left_alive, tries) + return + + # after running out of patience, we try a killProcess + logger.debug("Process did not died. Sending a SIGKILL.") + try: + self.killit() + except OSError: + logger.error("Could not kill process!") + + + def _start_pollers(self): + """ + Iterate through the registered observers + and start the looping call for them. + """ + for poller in self._pollers: + poller.start(VPNManager.POLL_TIME) + + + def _stop_pollers(self): + """ + Iterate through the registered observers + and stop the looping calls if they are running. + """ + for poller in self._pollers: + if poller.running: + poller.stop() + self._pollers = [] |