# -*- coding: utf-8 -*- # process.py # Copyright (C) 2013-2017 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 Process management. A custom processProtocol launches the VPNProcess and connects to its management interface. """ import sys from twisted.internet import protocol, reactor from twisted.internet import error as internet_error from twisted.logger import Logger from leap.bitmask.vpn.utils import get_vpn_launcher from leap.bitmask.vpn import _status from leap.bitmask.vpn import _management from leap.bitmask.vpn.launchers import darwin from leap.bitmask.vpn.constants import IS_MAC, IS_LINUX # OpenVPN verbosity level - from flags.py OPENVPN_VERBOSITY = 1 class _VPNProcess(protocol.ProcessProtocol, _management.VPNManagement): """ A ProcessProtocol class that can be used to spawn a process that will launch openvpn and connect to its management interface to control it programmatically. """ log = Logger() # HACK - reactor is expected to set this up when the process is spawned. # should try to get it from within this class. pid = None # TODO do we really need the vpnconfig/providerconfig objects in here??? def __init__(self, vpnconfig, providerconfig, socket_host, socket_port, openvpn_verb, remotes, restartfun=None): """ :param vpnconfig: vpn configuration object :type vpnconfig: VPNConfig :param providerconfig: provider specific configuration :type providerconfig: ProviderConfig :param socket_host: either socket path (unix) or socket IP :type socket_host: str :param socket_port: either string "unix" if it's a unix socket, or port otherwise :type socket_port: str :param openvpn_verb: the desired level of verbosity in the openvpn invocation :type openvpn_verb: int """ # TODO handle management as a component _management.VPNManagement.__init__(self) self.set_connection(socket_host, socket_port) self._vpnconfig = vpnconfig self._providerconfig = providerconfig self._launcher = get_vpn_launcher() self._last_state = None self._last_status = None self._alive = False # XXX use flags, maybe, instead of passing # the parameter around. self._openvpn_verb = openvpn_verb self._restartfun = restartfun self._status = _status.VPNStatus() self.set_watcher(self._status) self.restarting = False self._remotes = remotes @property def status(self): return self._status.status @property def traffic_status(self): return self._status.get_traffic_status() @traffic_status.setter def traffic_status(self, value): self._status.set_traffic_status(value) # processProtocol methods def connectionMade(self): """ Called when the connection is made. """ self._alive = True self.aborted = False self.connect_retry(max_retries=10) def outReceived(self, data): """ Called when new data is available on stdout. We only use this to drive the status state machine in linux, OSX uses the management interface. :param data: the data read on stdout """ # TODO deprecate, use log through management interface too. if IS_LINUX: # truncate the newline line = data[:-1] # TODO -- internalize this into _status!!! so that it can be shared if 'SIGTERM[soft,ping-restart]' in line: self.restarting = True self.log.info(line) def processExited(self, failure): """ Called when the child process exits. """ err = failure.trap( internet_error.ProcessDone, internet_error.ProcessTerminated) if err == internet_error.ProcessDone: status, errmsg = 'off', None elif err == internet_error.ProcessTerminated: status, errmsg = 'failure', failure.value.exitCode if errmsg: self.log.debug('processExited, status %d' % (errmsg,)) else: self.log.warn('%r' % failure.value) self._status.set_status(status, errmsg) self._alive = False def processEnded(self, reason): """ Called when the child process exits and all file descriptors associated with it have been closed. """ exit_code = reason.value.exitCode if isinstance(exit_code, int): self.log.debug('processEnded, status %d' % (exit_code,)) if self.restarting: self.log.debug('Restarting VPN process') reactor.callLater(2, self._restartfun) # polling def pollStatus(self): """ Polls connection status. """ if self._alive: self.get_status() def pollState(self): """ Polls connection state. """ if self._alive: self.get_state() def pollLog(self): if self._alive: self.process_log() # launcher def preUp(self): pass def getCommand(self): """ Gets the vpn command from the aproppriate launcher. Might throw: VPNLauncherException, OpenVPNNotFoundException. :rtype: list of str """ command = self._launcher.get_vpn_command( vpnconfig=self._vpnconfig, providerconfig=self._providerconfig, socket_host=self._host, socket_port=self._port, openvpn_verb=self._openvpn_verb, remotes=self._remotes) encoding = sys.getfilesystemencoding() for i, c in enumerate(command): if not isinstance(c, str): command[i] = c.encode(encoding) self.log.debug("Running VPN with command: ") self.log.debug("{0}".format(" ".join(command))) return command def getGateways(self): """ Get the gateways from the appropiate launcher. :rtype: list """ gateways_ports = self._launcher.get_gateways( self._vpnconfig, self._providerconfig) # filter out ports since we don't need that info return [gateway for gateway, port in gateways_ports] # shutdown def killProcess(self): """ Sends the KILL signal to the running process. """ try: self.transport.signalProcess('KILL') except internet_error.ProcessExitedAlready: self.log.debug('Process Exited Already') if IS_LINUX: VPNProcess = _VPNProcess elif IS_MAC: class _VPNCanary(_VPNProcess): """ Special form of _VPNProcess, for Darwin Launcher (windows might use same strategy). This is a Canary Process that does not run openvpn itself, but it's notified by the privileged process when the process dies. This is a workaround to allow the state machine to be notified when openvpn process is spawned by the privileged helper. """ def setupHelper(self): # TODO use get_vpn_launcher instead self.helper = darwin.HelperCommand() def preUp(self): self.setupHelper() cmd = self.getVPNCommand() self.helper.send('openvpn_start %s' % ' '.join(cmd)) def connectionMade(self): super(_VPNProcess, self).connectionMade() self.setupHelper() reactor.callLater(2, self.registerPID) def registerPID(self): cmd = 'openvpn_set_watcher %s' % self.pid self.helper.send(cmd) def killProcess(self): cmd = 'openvpn_force_stop' self.helper.send(cmd) def getVPNCommand(self): vpncmd = _VPNProcess.getCommand(self) return vpncmd def getCommand(self): canary = '''import sys, signal, time def receive_signal(signum, stack): sys.exit() signal.signal(signal.SIGTERM, receive_signal) while True: time.sleep(60)''' return ['python', '-c', '%s' % canary] VPNProcess = _VPNCanary