From b7b8296c24017ccc2a04cdd0682f4905b8fcf8d7 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 22 Aug 2017 17:42:19 -0400 Subject: [refactor] merge tunnel and control modules --- src/leap/bitmask/vpn/tunnel.py | 236 ++++++++++++++++++++++++++++++++--------- 1 file changed, 183 insertions(+), 53 deletions(-) (limited to 'src/leap/bitmask/vpn/tunnel.py') diff --git a/src/leap/bitmask/vpn/tunnel.py b/src/leap/bitmask/vpn/tunnel.py index c2b0f58b..f56e7bc2 100644 --- a/src/leap/bitmask/vpn/tunnel.py +++ b/src/leap/bitmask/vpn/tunnel.py @@ -22,28 +22,48 @@ VPN Tunnel. import os import tempfile -from ._control import VPNControl +from twisted.internet import reactor, defer +from twisted.logger import Logger + from ._config import _TempVPNConfig, _TempProviderConfig -from .constants import IS_WIN +from .constants import IS_WIN, IS_LINUX +from .process import VPNProcess -# TODO refactor - this class is still a very light proxy around the -# underlying VPNControl. The main methods here are start/stop, so this -# looks like it could better use the Service interface. # TODO gateway selection should be done in this class. -# TODO DO NOT pass VPNConfig/ProviderConfig beyond this class. -# TODO split sync/async vpn control mechanisms. -# TODO ConfiguredVPNConnection ? + +# TODO ----------------- refactor -------------------- +# [ ] register change state listener +# emit_async(catalog.VPN_STATUS_CHANGED) +# [ ] catch ping-restart +# 'NETWORK_UNREACHABLE': ( +# 'Network is unreachable (code=101)',), +# 'PROCESS_RESTART_TLS': ( +# "SIGTERM[soft,tls-error]",), -class TunnelManager(object): +class ConfiguredTunnel(object): """ - A VPN Tunnel holds the configuration for a VPN connection, and allows to - control that connection. + A ConfiguredTunne holds the configuration for a VPN connection, and allows + to control that connection. + + This is the high-level object that the service knows about. + It exposes the start and terminate methods for the VPN Tunnel. + + 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 + RESTART_WAIT = 2 # secs + + log = Logger() + def __init__(self, provider, remotes, cert_path, key_path, ca_path, extra_flags): """ @@ -51,66 +71,176 @@ class TunnelManager(object): ((ip1, portA), (ip2, portB), ...) :type remotes: tuple of tuple(str, int) """ - # TODO we can set all the needed ports, gateways and paths in here - # TODO need gateways here - # sorting them doesn't belong in here - # gateways = ((ip1, portA), (ip2, portB), ...) - - ports = [] - self._remotes = remotes + ports = [] self._vpnconfig = _TempVPNConfig(extra_flags, cert_path, ports) self._providerconfig = _TempProviderConfig(provider, ca_path) - host, port = self._get_management_location() + host, port = _get_management_location() + self._host = host + self._port = port - self._vpncontrol = VPNControl( - remotes=remotes, vpnconfig=self._vpnconfig, - providerconfig=self._providerconfig, - socket_host=host, socket_port=port) + self._vpnproc = None + self._user_stopped = False def start(self): - """ - Start the VPN process. - """ - result = self._vpncontrol.start() - return result + return self._start_vpn() def stop(self): - """ - Bring openvpn down. + return self._stop_vpn(shutdown=False, restart=False) - :returns: True if succeeded, False otherwise. - :rtype: bool - """ - # TODO how to return False if this fails - result = self._vpncontrol.stop(False, False) # TODO review params - return result + # status @property def status(self): - return self._vpncontrol.status + if not self._vpnproc: + return {'status': 'off', 'error': None} + return self._vpnproc.status @property def traffic_status(self): - return self._vpncontrol.traffic_status + return self._vpnproc.traffic_status + + # VPN Control + + def _start_vpn(self): + self.log.debug('VPN: start') + + self._user_stopped = False + + args = [self._vpnconfig, self._providerconfig, self._host, + self._port] + kwargs = {'openvpn_verb': 4, 'remotes': self._remotes, + 'restartfun': self.restart} + vpnproc = VPNProcess(*args, **kwargs) + + try: + vpnproc.preUp() + except Exception as e: + self.log.error('Error on vpn pre-up {0!r}'.format(e)) + raise + try: + cmd = vpnproc.getCommand() + except Exception as e: + self.log.error( + 'Error while getting vpn command... {0!r}'.format(e)) + raise + + env = os.environ + try: + runningproc = reactor.spawnProcess(vpnproc, cmd[0], cmd, env) + except Exception as e: + self.log.error( + 'Error while spawning vpn process... {0!r}'.format(e)) + return False + + # TODO get pid from management instead + vpnproc.pid = runningproc.pid + self._vpnproc = vpnproc + return True + + @defer.inlineCallbacks + def _restart_vpn(self): + yield self.stop(shutdown=False, restart=True) + reactor.callLater( + self.RESTART_WAIT, self.start) + + def _stop_vpn(self, shutdown=False, restart=False): + """ + Stops the openvpn subprocess. + + Attempts to send a SIGTERM first, and after a timeout + it sends a SIGKILL. - def _get_management_location(self): + :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 """ - Return a tuple with the host (socket) and port to be used for VPN. + # TODO how to return False if this fails + # XXX maybe return a deferred + + # We assume that the only valid stops are initiated + # by an user action, not hard restarts + self._user_stopped = not restart + if self._vpnproc is not None: + self._vpnproc.restarting = restart + + try: + if self._vpnproc is not None: + self._vpnproc.preDown() + except Exception as e: + self.log.error('Error on vpn pre-down {0!r}'.format(e)) + raise + + d = defer.succeed(True) + if IS_LINUX: + # TODO factor this out to a linux-only launcher mechanism. + # First we try to be polite and send a SIGTERM... + if self._vpnproc is not None: + self._sentterm = True + self._vpnproc.terminate() + + # we trigger a countdown to be unpolite + # if strictly needed. + d = defer.Deferred() + reactor.callLater( + self.TERMINATE_WAIT, self._kill_if_left_alive, d) + return d + + def _wait_and_kill(self, deferred, tries=0): + """ + Check if the process is still alive, and send a + SIGKILL after a waiting several times during a timeout period. - :return: (host, port) - :rtype: tuple (str, str) + :param tries: counter of tries, used in recursion + :type tries: int """ - if IS_WIN: - host = "localhost" - port = "9876" - else: - # XXX cleanup this on exit too - # XXX atexit.register ? - host = os.path.join(tempfile.mkdtemp(prefix="leap-tmp"), - 'openvpn.socket') - port = "unix" - - return host, port + if tries < self.TERMINATE_MAXTRIES: + if self._vpnproc.transport.pid is None: + deferred.callback(True) + return + else: + self.log.debug('Process did not die, waiting...') + + tries += 1 + reactor.callLater( + self.TERMINATE_WAIT, + self._wait_and_kill, deferred, tries) + return + + # after running out of patience, we try a killProcess + self._kill(deferred) + + def _kill(self, d): + self.log.debug('Process did not die. Sending a SIGKILL.') + try: + if self._vpnproc is None: + self.log.debug("There's no vpn process running to kill.") + else: + self._vpnproc.aborted = True + self._vpnproc.kill() + except OSError: + self.log.error('Could not kill process!') + d.callback(True) + + +# utils + + +def _get_management_location(): + """ + Return a tuple with the host (socket) and port to be used for VPN. + + :return: (host, port) + :rtype: tuple (str, str) + """ + if IS_WIN: + host = "localhost" + port = "9876" + else: + host = os.path.join( + tempfile.mkdtemp(prefix="leap-tmp"), 'openvpn.socket') + port = "unix" + return host, port -- cgit v1.2.3