# -*- coding: utf-8 -*- # manager.py # Copyright (C) 2015 LEAP # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ VPN Tunnel. """ import os import tempfile from twisted.internet import reactor, defer from twisted.logger import Logger from leap.bitmask.system import IS_WIN from ._config import _TempVPNConfig, _TempProviderConfig from .process import VPNProcess # The restarts are not really needed, since we're configuring # openvpn to restart itself after a period of inactivity. However, if the # openvpn process is killed by whatever reason, # we'll automatically try to # restart the process. RESTART_WAIT = 2 # in secs class ConfiguredTunnel(object): """ A ConfiguredTunnel 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 """ log = Logger() def __init__(self, provider, remotes, cert_path, key_path, ca_path, extra_flags): """ :param remotes: a list of gateways tuple (ip, port) looking like this: ((ip1, portA), (ip2, portB), ...) :type remotes: tuple of tuple(str, int) """ self._remotes = remotes ports = [] self._vpnconfig = _TempVPNConfig(extra_flags, cert_path, ports) self._providerconfig = _TempProviderConfig(provider, ca_path) host, port = _get_management_location() self._host = host self._port = port self._vpnproc = None @defer.inlineCallbacks def start(self): started = yield self._start_vpn() defer.returnValue(started) @defer.inlineCallbacks def stop(self, restart=False): stopped = yield self._stop_vpn(restart=restart) defer.returnValue(stopped) # status @property def status(self): if not self._vpnproc: status = {'status': 'off', 'error': None} else: status = self._vpnproc.status # Currently, there's some UI flickering that needs to be debugged #9049 # print ">>>> STATUS", status return status @property def traffic_status(self): return self._vpnproc.traffic_status # VPN Control @defer.inlineCallbacks def _start_vpn(self): try: self.log.debug('VPN: start') args = [self._vpnconfig, self._providerconfig, self._host, self._port] kwargs = {'openvpn_verb': 4, 'remotes': self._remotes, 'restartfun': self._restart_vpn} vpnproc = VPNProcess(*args, **kwargs) self._vpnproc = vpnproc self.__start_pre_up(vpnproc) cmd = self.__start_get_cmd(vpnproc) running = yield self.__start_spawn_proc(vpnproc, cmd) vpnproc.pid = running.pid defer.returnValue(True) except Exception as exc: if self._vpnproc: self._vpnproc.failed = True self._vpnproc.errmsg = exc.message raise def __start_pre_up(self, proc): try: proc.preUp() except Exception as exc: self.log.error('Error on vpn pre-up {0!r}'.format(exc)) raise def __start_get_cmd(self, proc): try: cmd = proc.getCommand() except Exception as exc: self.log.error( 'Error while getting vpn command... {0!r}'.format(exc)) raise exc return cmd def __start_spawn_proc(self, proc, cmd): env = os.environ try: running_p = reactor.spawnProcess(proc, cmd[0], cmd, env) except Exception as exc: self.log.error( 'Error while spawning vpn process... {0!r}'.format(exc)) raise exc return running_p @defer.inlineCallbacks def _restart_vpn(self): yield self.stop(restart=True) reactor.callLater( RESTART_WAIT, self.start) @defer.inlineCallbacks def _stop_vpn(self, restart=False): """ Stops the openvpn subprocess. Attempts to send a SIGTERM first, and after a timeout it sends a SIGKILL. :param restart: whether this stop is part of a hard restart. :type restart: bool """ # TODO how to return False if this fails # XXX maybe return a deferred if self._vpnproc is None: self.log.debug('Tried to stop VPN but no process found') defer.returnValue(False) self._vpnproc.restarting = restart self.__stop_pre_down(self._vpnproc) stopped = yield self._vpnproc.terminate_or_kill() defer.returnValue(stopped) def __stop_pre_down(self, proc): try: proc.preDown() except Exception as e: self.log.error('Error on vpn pre-down {0!r}'.format(e)) raise # 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