From 701fe5ebc70fb49bb32e81e6d6605f27ad09925b Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Sat, 4 Feb 2017 17:01:25 +0100 Subject: [feature] parse status - simple status parsing - add separate firewall status - set status for abnormal termination --- src/leap/bitmask/cli/command.py | 6 ++- src/leap/bitmask/vpn/_control.py | 54 +++++++++++--------------- src/leap/bitmask/vpn/_management.py | 32 ++++++--------- src/leap/bitmask/vpn/_observer.py | 73 ----------------------------------- src/leap/bitmask/vpn/_status.py | 77 +++++++++++++++++++++++++++++++++++++ src/leap/bitmask/vpn/eip.py | 23 ++++------- src/leap/bitmask/vpn/fw/firewall.py | 19 +++++++-- src/leap/bitmask/vpn/manager.py | 33 +++------------- src/leap/bitmask/vpn/process.py | 40 +++++++++++-------- src/leap/bitmask/vpn/service.py | 10 +++-- src/leap/bitmask/vpn/status.py | 42 -------------------- src/leap/bitmask/vpn/utils.py | 1 - 12 files changed, 177 insertions(+), 233 deletions(-) delete mode 100644 src/leap/bitmask/vpn/_observer.py create mode 100644 src/leap/bitmask/vpn/_status.py delete mode 100644 src/leap/bitmask/vpn/status.py (limited to 'src/leap') diff --git a/src/leap/bitmask/cli/command.py b/src/leap/bitmask/cli/command.py index ff213a35..a4757f80 100644 --- a/src/leap/bitmask/cli/command.py +++ b/src/leap/bitmask/cli/command.py @@ -44,7 +44,11 @@ def default_dict_printer(result): for key, value in result.items(): if value is None: value = str(value) - print(Fore.RESET + key.ljust(10) + Fore.GREEN + value + Fore.RESET) + if value in ('OFF', 'OFFLINE', 'ABORTED'): + color = Fore.RED + else: + color = Fore.GREEN + print(Fore.RESET + key.ljust(10) + color + value + Fore.RESET) class Command(object): diff --git a/src/leap/bitmask/vpn/_control.py b/src/leap/bitmask/vpn/_control.py index 6e942f48..e05aeed1 100644 --- a/src/leap/bitmask/vpn/_control.py +++ b/src/leap/bitmask/vpn/_control.py @@ -1,5 +1,5 @@ - import os +import subprocess from twisted.internet.task import LoopingCall from twisted.internet import reactor @@ -18,7 +18,7 @@ POLL_TIME = 2.5 if IS_MAC else 1.0 class VPNControl(object): """ - This is the high-level object that the service is dealing with. + This is the high-level object that the service knows about. It exposes the start and terminate methods. On start, it spawns a VPNProcess instance that will use a vpnlauncher @@ -37,10 +37,8 @@ class VPNControl(object): self._vpnproc = None self._pollers = [] - self._signaler = kwargs['signaler'] # self._openvpn_verb = flags.OPENVPN_VERBOSITY self._openvpn_verb = None - self._user_stopped = False self._remotes = kwargs['remotes'] @@ -58,7 +56,6 @@ class VPNControl(object): self._user_stopped = False self._stop_pollers() kwargs['openvpn_verb'] = self._openvpn_verb - kwargs['signaler'] = self._signaler kwargs['remotes'] = self._remotes # start the main vpn subprocess @@ -72,15 +69,7 @@ class VPNControl(object): # errors here are catched, since we currently handle them # at the frontend layer. This *should* move to be handled entirely # in the backend. - # exception is indeed technically catched in backend, then converted - # into a signal, that is catched in the eip_status widget in the - # frontend, and converted into a signal to abort the connection that is - # sent to the backend again. - - # the whole exception catching should be done in the backend, without - # the ping-pong to the frontend, and without adding any logical checks - # in the frontend. We should just communicate UI changes to frontend, - # and abstract us away from anything else. + try: cmd = vpnproc.getCommand() except Exception as e: @@ -103,9 +92,14 @@ class VPNControl(object): self._pollers.extend(poll_list) self._start_pollers() + @property + def status(self): + if not self._vpnproc: + return 'OFFLINE' + return self._vpnproc.status + - # TODO -- rename to stop ?? - def terminate(self, shutdown=False, restart=False): + def stop(self, shutdown=False, restart=False): """ Stops the openvpn subprocess. @@ -136,20 +130,6 @@ class VPNControl(object): else: logger.debug("VPN is not running.") - - # TODO should this be public?? - def killit(self): - """ - Sends a kill signal to the process. - """ - self._stop_pollers() - if self._vpnproc is None: - logger.debug("There's no vpn process running to kill.") - else: - self._vpnproc.aborted = True - self._vpnproc.killProcess() - - def bitmask_root_vpn_down(self): """ Bring openvpn down using the privileged wrapper. @@ -164,6 +144,18 @@ class VPNControl(object): BM_ROOT, "openvpn", "stop"]) return True if exitCode is 0 else False + def _killit(self): + """ + Sends a kill signal to the process. + """ + self._stop_pollers() + if self._vpnproc is None: + logger.debug("There's no vpn process running to kill.") + else: + self._vpnproc.aborted = True + self._vpnproc.killProcess() + + def _kill_if_left_alive(self, tries=0): """ @@ -188,7 +180,7 @@ class VPNControl(object): # after running out of patience, we try a killProcess logger.debug("Process did not died. Sending a SIGKILL.") try: - self.killit() + self._killit() except OSError: logger.error("Could not kill process!") diff --git a/src/leap/bitmask/vpn/_management.py b/src/leap/bitmask/vpn/_management.py index 920bd9d1..905372b1 100644 --- a/src/leap/bitmask/vpn/_management.py +++ b/src/leap/bitmask/vpn/_management.py @@ -45,16 +45,8 @@ class VPNManagement(object): # Timers, in secs CONNECTION_RETRY_TIME = 1 - def __init__(self, signaler=None): - """ - Initializes the VPNManager. - - :param signaler: Signaler object used to send notifications to the - backend - :type signaler: backend.Signaler - """ + def __init__(self): self._tn = None - self._signaler = signaler self.aborted = False def _seek_to_eof(self): @@ -227,8 +219,8 @@ class VPNManagement(object): def _parse_state_and_notify(self, output): """ - Parses the output of the state command and emits state_changed - signal when the state changes. + Parses the output of the state command, and trigger a state transition + when the state changes. :param output: list of lines that the state command printed as its output @@ -248,9 +240,9 @@ class VPNManagement(object): state = status_step if state != self._last_state: - if self._signaler is not None: - self._signaler.signal( - self._signaler.eip_state_changed, state) + # XXX this status object is the vpn status observer + if self._status: + self._status.set_status(state, None) self._last_state = state def _parse_status_and_notify(self, output): @@ -286,12 +278,12 @@ class VPNManagement(object): elif text == "TUN/TAP write bytes": tun_tap_write = value # upload - status = (tun_tap_read, tun_tap_write) - if status != self._last_status: - if self._signaler is not None: - self._signaler.signal( - self._signaler.eip_status_changed, status) - self._last_status = status + traffic_status = (tun_tap_read, tun_tap_write) + if traffic_status != self._last_status: + # XXX this status object is the vpn status observer + if self._status: + self._status.set_traffic_status(traffic_status) + self._last_status = traffic_status def get_state(self): """ diff --git a/src/leap/bitmask/vpn/_observer.py b/src/leap/bitmask/vpn/_observer.py deleted file mode 100644 index c50a50d9..00000000 --- a/src/leap/bitmask/vpn/_observer.py +++ /dev/null @@ -1,73 +0,0 @@ -from itertools import chain, repeat -from twisted.logger import Logger - -logger = Logger() - - -class VPNObserver(object): - """ - A class containing different patterns in the openvpn output that - we can react upon. - """ - - _events = { - 'NETWORK_UNREACHABLE': ( - 'Network is unreachable (code=101)',), - 'PROCESS_RESTART_TLS': ( - "SIGTERM[soft,tls-error]",), - 'PROCESS_RESTART_PING': ( - "SIGTERM[soft,ping-restart]",), - 'INITIALIZATION_COMPLETED': ( - "Initialization Sequence Completed",), - } - - def __init__(self, signaler=None): - self._signaler = signaler - - def watch(self, line): - """ - Inspects line searching for the different patterns. If a match - is found, try to emit the corresponding signal. - - :param line: a line of openvpn output - :type line: str - """ - chained_iter = chain(*[ - zip(repeat(key, len(l)), l) - for key, l in self._events.iteritems()]) - for event, pattern in chained_iter: - if pattern in line: - logger.debug('pattern matched! %s' % pattern) - break - else: - return - - sig = self._get_signal(event) - if sig is not None: - if self._signaler is not None: - self._signaler.signal(sig) - return - else: - logger.debug('We got %s event from openvpn output but we could ' - 'not find a matching signal for it.' % event) - - def _get_signal(self, event): - """ - Tries to get the matching signal from the eip signals - objects based on the name of the passed event (in lowercase) - - :param event: the name of the event that we want to get a signal for - :type event: str - :returns: a Signaler signal or None - :rtype: str or None - """ - if self._signaler is None: - return - sig = self._signaler - signals = { - "network_unreachable": sig.eip_network_unreachable, - "process_restart_tls": sig.eip_process_restart_tls, - "process_restart_ping": sig.eip_process_restart_ping, - "initialization_completed": sig.eip_connected - } - return signals.get(event.lower()) diff --git a/src/leap/bitmask/vpn/_status.py b/src/leap/bitmask/vpn/_status.py new file mode 100644 index 00000000..11d564fa --- /dev/null +++ b/src/leap/bitmask/vpn/_status.py @@ -0,0 +1,77 @@ +from itertools import chain, repeat +from twisted.logger import Logger + +logger = Logger() + + +# TODO implement a state machine in this class + + +class VPNStatus(object): + """ + A class containing different patterns in the openvpn output that + we can react upon. + """ + + _events = { + 'NETWORK_UNREACHABLE': ( + 'Network is unreachable (code=101)',), + 'PROCESS_RESTART_TLS': ( + "SIGTERM[soft,tls-error]",), + 'PROCESS_RESTART_PING': ( + "SIGTERM[soft,ping-restart]",), + 'INITIALIZATION_COMPLETED': ( + "Initialization Sequence Completed",), + } + + def __init__(self): + self.status = 'OFFLINE' + self._traffic_down = None + self._traffic_up = None + + def watch(self, line): + """ + Inspects line searching for the different patterns. If a match + is found, try to emit the corresponding signal. + + :param line: a line of openvpn output + :type line: str + """ + chained_iter = chain(*[ + zip(repeat(key, len(l)), l) + for key, l in self._events.iteritems()]) + for event, pattern in chained_iter: + if pattern in line: + break + else: + return + + status, errcode = self._status_codes(event) + self.set_status(status, errcode) + + + def set_status(self, status, errcode): + self.status = status + self.errcode = errcode + + def set_traffic_status(self, status): + down, up = status + self._traffic_up = up + self._traffic_down = down + + def get_traffic_status(self): + # XXX return Human readable too + return {'down': self._traffic_down, + 'up': self._traffic_up} + + def _status_codes(self, event): + # TODO check good transitions + # TODO check valid states + + _table = { + "network_unreachable": ('OFFLINE', 'network unreachable'), + "process_restart_tls": ('RESTARTING', 'restart tls'), + "process_restart_ping": ('CONNECTING', None), + "initialization_completed": ('ONLINE', None) + } + return _table.get(event.lower()) diff --git a/src/leap/bitmask/vpn/eip.py b/src/leap/bitmask/vpn/eip.py index b080aa65..c2aa4fb3 100644 --- a/src/leap/bitmask/vpn/eip.py +++ b/src/leap/bitmask/vpn/eip.py @@ -20,19 +20,15 @@ from colorama import Fore from leap.bitmask.vpn.manager import VPNManager from leap.bitmask.vpn.fw.firewall import FirewallManager -from leap.bitmask.vpn.status import StatusQueue -from leap.bitmask.vpn.zmq_pub import ZMQPublisher class EIPManager(object): def __init__(self, remotes, cert, key, ca, flags): + self._vpn = VPNManager( + remotes, cert, key, ca, flags) self._firewall = FirewallManager(remotes) - self._status_queue = StatusQueue() - self._pub = ZMQPublisher(self._status_queue) - self._vpn = VPNManager(remotes, cert, key, ca, flags, - self._status_queue) def start(self): """ @@ -40,7 +36,6 @@ class EIPManager(object): This may raise exceptions, see errors.py """ - # self._pub.start() print(Fore.BLUE + "Firewall: starting..." + Fore.RESET) fw_ok = self._firewall.start() if not fw_ok: @@ -59,10 +54,6 @@ class EIPManager(object): print(Fore.GREEN + "VPN: started" + Fore.RESET) def stop(self): - """ - Stop EIP service - """ - # self._pub.stop() print(Fore.BLUE + "Firewall: stopping..." + Fore.RESET) fw_ok = self._firewall.stop() @@ -81,8 +72,10 @@ class EIPManager(object): print(Fore.GREEN + "VPN: stopped." + Fore.RESET) return True - def get_state(self): - pass - def get_status(self): - pass + vpn_status = self._vpn.status + fw_status = self._firewall.status + result = {'EIP': vpn_status, + 'firewall': fw_status} + return result + diff --git a/src/leap/bitmask/vpn/fw/firewall.py b/src/leap/bitmask/vpn/fw/firewall.py index 4335b8e9..5eace20a 100644 --- a/src/leap/bitmask/vpn/fw/firewall.py +++ b/src/leap/bitmask/vpn/fw/firewall.py @@ -13,7 +13,7 @@ # 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 . +# alng with this program. If not, see . """ Firewall Manager @@ -44,6 +44,7 @@ class FirewallManager(object): :param remotes: the gateway(s) that we will allow :type remotes: list """ + self.status = 'OFF' self._remotes = remotes def start(self, restart=False): @@ -65,7 +66,13 @@ class FirewallManager(object): # FIXME -- use a processprotocol exitCode = subprocess.call(cmd + gateways) - return True if exitCode is 0 else False + + if exitCode == 0: + self.status = 'ON' + return True + else: + self.status = 'OFF' + return False # def tear_down_firewall(self): def stop(self): @@ -78,9 +85,13 @@ class FirewallManager(object): exitCode = subprocess.call(["pkexec", self.BITMASK_ROOT, "firewall", "stop"]) - return True if exitCode is 0 else False + if exitCode == 0: + self.status = 'OFF' + return True + else: + self.status = 'ON' + return False - # def is_fw_down(self): def is_up(self): """ Return whether the firewall is up or not. diff --git a/src/leap/bitmask/vpn/manager.py b/src/leap/bitmask/vpn/manager.py index a27789c3..04e88097 100644 --- a/src/leap/bitmask/vpn/manager.py +++ b/src/leap/bitmask/vpn/manager.py @@ -33,8 +33,7 @@ from .constants import IS_WIN class VPNManager(object): - def __init__(self, remotes, cert_path, key_path, ca_path, extra_flags, - mock_signaler): + def __init__(self, remotes, cert_path, key_path, ca_path, extra_flags): """ Initialize the VPNManager object. @@ -51,8 +50,7 @@ class VPNManager(object): self._eipconfig = _TempEIPConfig(extra_flags, cert_path, ports) self._providerconfig = _TempProviderConfig(domain, ca_path) - signaler = None # XXX handle signaling somehow... - self._vpn = VPNControl(remotes=remotes, signaler=signaler) + self._vpn = VPNControl(remotes=remotes) def start(self): @@ -82,34 +80,15 @@ class VPNManager(object): :returns: True if succeeded, False otherwise. :rtype: bool """ - self._vpn.terminate(False, False) # TODO review params - # TODO how to return False if this fails + self._vpn.stop(False, False) # TODO review params return True - def is_up(self): - """ - Return whether the VPN is up or not. - - :rtype: bool - """ - pass - - def kill(self): - """ - Sends a kill signal to the openvpn process. - """ - pass - # self._vpn.killit() - def terminate(self): - """ - Stop the openvpn subprocess. + @property + def status(self): + return self._vpn.status - Attempts to send a SIGTERM first, and after a timeout it sends a - SIGKILL. - """ - pass def _get_management_location(self): """ diff --git a/src/leap/bitmask/vpn/process.py b/src/leap/bitmask/vpn/process.py index 813025d7..185ba624 100644 --- a/src/leap/bitmask/vpn/process.py +++ b/src/leap/bitmask/vpn/process.py @@ -17,6 +17,7 @@ """ VPN Process management. + A custom processProtocol launches the VPNProcess and connects to its management interface. """ @@ -39,7 +40,7 @@ from leap.bitmask.vpn.utils import first, force_eval from leap.bitmask.vpn.utils import get_vpn_launcher from leap.bitmask.vpn.launchers import linux from leap.bitmask.vpn._telnet import UDSTelnet -from leap.bitmask.vpn import _observer +from leap.bitmask.vpn import _status from leap.bitmask.vpn import _management logger = Logger() @@ -57,7 +58,7 @@ class VPNProcess(protocol.ProcessProtocol, _management.VPNManagement): """ def __init__(self, eipconfig, providerconfig, socket_host, socket_port, - signaler, openvpn_verb, remotes): + openvpn_verb, remotes): """ :param eipconfig: eip configuration object :type eipconfig: EIPConfig @@ -72,15 +73,11 @@ class VPNProcess(protocol.ProcessProtocol, _management.VPNManagement): socket, or port otherwise :type socket_port: str - :param signaler: Signaler object used to receive notifications to the - backend - :type signaler: backend.Signaler - :param openvpn_verb: the desired level of verbosity in the openvpn invocation :type openvpn_verb: int """ - _management.VPNManagement.__init__(self, signaler=signaler) + _management.VPNManagement.__init__(self) self._eipconfig = eipconfig self._providerconfig = providerconfig @@ -97,11 +94,15 @@ class VPNProcess(protocol.ProcessProtocol, _management.VPNManagement): # the parameter around. self._openvpn_verb = openvpn_verb - self._vpn_observer = _observer.VPNObserver(signaler) + self._status = _status.VPNStatus() self.is_restart = False self._remotes = remotes + @property + def status(self): + return self._status.status + # processProtocol methods def connectionMade(self): @@ -125,20 +126,27 @@ class VPNProcess(protocol.ProcessProtocol, _management.VPNManagement): # truncate the newline line = data[:-1] logger.info(line) - self._vpn_observer.watch(line) + self._status.watch(line) - def processExited(self, reason): + def processExited(self, failure): """ Called when the child process exits. .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa """ - exit_code = reason.value.exitCode - if isinstance(exit_code, int): - logger.debug("processExited, status %d" % (exit_code,)) - if self._signaler is not None: - self._signaler.signal( - self._signaler.eip_process_finished, exit_code) + err = failure.trap( + internet_error.ProcessDone, internet_error.ProcessTerminated) + + if err == internet_error.ProcessDone: + status, errmsg = 'OFFLINE', None + elif err == internet_error.ProcessTerminated: + status, errmsg = 'ABORTED', failure.value.exitCode + if errmsg: + logger.debug("processExited, status %d" % (errmsg,)) + else: + logger.warn('%r' % failure.value) + + self._status.set_status(status, errmsg) self._alive = False def processEnded(self, reason): diff --git a/src/leap/bitmask/vpn/service.py b/src/leap/bitmask/vpn/service.py index 3edae352..170c5afd 100644 --- a/src/leap/bitmask/vpn/service.py +++ b/src/leap/bitmask/vpn/service.py @@ -45,6 +45,7 @@ class EIPService(HookableService): super(EIPService, self).__init__() self._started = False + self._eip = None if basepath is None: self._basepath = get_path_prefix() @@ -74,8 +75,11 @@ class EIPService(HookableService): return {'result': 'stopped'} def do_status(self): - # TODO -- get status from a dedicated STATUS CLASS - return {'result': 'running'} + if self._eip: + status = self._eip.get_status() + else: + status = {'EIP': 'OFF'} + return status def do_check(self): """Check whether the EIP Service is properly configured, @@ -100,7 +104,7 @@ class EIPService(HookableService): os.makedirs(cert_dir, mode=0700) with open(cert_path, 'w') as outf: outf.write(cert_str) - check_and_fix_urw_only(cert_path) + heck_and_fix_urw_only(cert_path) defer.returnValue({'get_cert': 'ok'}) def do_install(self): diff --git a/src/leap/bitmask/vpn/status.py b/src/leap/bitmask/vpn/status.py deleted file mode 100644 index ff7f3111..00000000 --- a/src/leap/bitmask/vpn/status.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -Queue used to store status changes on EIP/VPN/Firewall and to be checked for -any app using this vpn library. - -This should be considered a temporary code meant to replace the signaling -system that announces events inside of vpn code and is catched on the bitmask -client. -""" - -import Queue - - -class StatusQueue(object): - def __init__(self): - self._status = Queue.Queue() - - # this attributes serve to simulate events in the old signaler used - self.eip_network_unreachable = "network_unreachable" - self.eip_process_restart_tls = "process_restart_tls" - self.eip_process_restart_ping = "process_restart_ping" - self.eip_connected = "initialization_completed" - self.eip_status_changed = "status_changed" # has parameter - self.eip_state_changed = "state_changed" # has parameter - self.eip_process_finished = "process_finished" # has parameter - - def get_noblock(self): - s = None - try: - s = self._status.get(False) - except Queue.Empty: - pass - - return s - - def get(self): - return self._status.get(timeout=1) - - def signal(self, status, data=None): - self._status.put({'status': status, 'data': data}) diff --git a/src/leap/bitmask/vpn/utils.py b/src/leap/bitmask/vpn/utils.py index b0ffa128..8fa51783 100644 --- a/src/leap/bitmask/vpn/utils.py +++ b/src/leap/bitmask/vpn/utils.py @@ -19,7 +19,6 @@ Common utils """ -import os def force_eval(items): """ -- cgit v1.2.3