# -*- 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 ._config import _TempVPNConfig, _TempProviderConfig from .constants import IS_WIN from .process import VPNProcess # 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]",), # TODO ----------------- refactor -------------------- class ConfiguredTunnel(object): """ 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. """ 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 def start(self): return self._start_vpn() def stop(self): return self._stop_vpn(restart=False) # status @property def status(self): if not self._vpnproc: return {'status': 'off', 'error': None} return self._vpnproc.status @property def traffic_status(self): return self._vpnproc.traffic_status # VPN Control def _start_vpn(self): 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) # XXX this should be a deferred running = self.__start_spawn_proc(vpnproc, cmd) if running: vpnproc.pid = running.pid return True else: return False 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 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 e: self.log.error( 'Error while spawning vpn process... {0!r}'.format(e)) return False return running_p @defer.inlineCallbacks def _restart_vpn(self): yield self.stop(restart=True) reactor.callLater( self.RESTART_WAIT, self.start) 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') return self._vpnproc.restarting = restart self.__stop_pre_down(self._vpnproc) self._vpnproc.terminate_or_kill() 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