summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/vpn/tunnel.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/bitmask/vpn/tunnel.py')
-rw-r--r--src/leap/bitmask/vpn/tunnel.py236
1 files changed, 183 insertions, 53 deletions
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