diff options
Diffstat (limited to 'src/leap/base')
-rw-r--r-- | src/leap/base/checks.py | 139 | ||||
-rw-r--r-- | src/leap/base/constants.py | 2 | ||||
-rw-r--r-- | src/leap/base/exceptions.py | 66 | ||||
-rw-r--r-- | src/leap/base/network.py | 80 | ||||
-rw-r--r-- | src/leap/base/tests/test_checks.py | 117 |
5 files changed, 404 insertions, 0 deletions
diff --git a/src/leap/base/checks.py b/src/leap/base/checks.py new file mode 100644 index 00000000..84f9dd46 --- /dev/null +++ b/src/leap/base/checks.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +import logging +import platform + +import netifaces +import ping +import requests + +from leap.base import constants +from leap.base import exceptions + +logger = logging.getLogger(name=__name__) + + +class LeapNetworkChecker(object): + """ + all network related checks + """ + # #718 + # XXX get provider gateway as a parameter + # for constructor. + # def __init__(self, *args, **kwargs): + # ... + # provider_gw = kwargs.pop('provider_gw', None) + # self.provider_gateway = provider_gw + + def run_all(self, checker=None): + if not checker: + checker = self + self.error = None # ? + + # for MVS + checker.check_tunnel_default_interface() + checker.check_internet_connection() + checker.is_internet_up() + + # XXX We are pinging the default gateway for our connection right? + # kali: 2012-10-05 20:59 -- I think we should get + # also the default gateway and ping it instead. + checker.ping_gateway() + + # something like: ? + # see __init__ above + # if self.provider_gateway: + # checker.ping_gateway(self.provider_gateway) + + def check_internet_connection(self): + try: + # XXX remove this hardcoded random ip + # ping leap.se or eip provider instead...? + requests.get('http://216.172.161.165') + + except (requests.HTTPError, requests.RequestException) as e: + raise exceptions.NoInternetConnection(e.message) + except requests.ConnectionError as e: + error = "Unidentified Connection Error" + if e.message == "[Errno 113] No route to host": + if not self.is_internet_up(): + error = "No valid internet connection found." + else: + error = "Provider server appears to be down." + logger.error(error) + raise exceptions.NoInternetConnection(error) + logger.debug('Network appears to be up.') + + def is_internet_up(self): + iface, gateway = self.get_default_interface_gateway() + self.ping_gateway(self) + + def check_tunnel_default_interface(self): + """ + Raises an TunnelNotDefaultRouteError + (including when no routes are present) + """ + if not platform.system() == "Linux": + raise NotImplementedError + + f = open("/proc/net/route") + route_table = f.readlines() + f.close() + #toss out header + route_table.pop(0) + + if not route_table: + raise exceptions.TunnelNotDefaultRouteError() + + line = route_table.pop(0) + iface, destination = line.split('\t')[0:2] + if not destination == '00000000' or not iface == 'tun0': + raise exceptions.TunnelNotDefaultRouteError() + + def get_default_interface_gateway(self): + """only impletemented for linux so far.""" + if not platform.system() == "Linux": + raise NotImplementedError + + # XXX use psutil + f = open("/proc/net/route") + route_table = f.readlines() + f.close() + #toss out header + route_table.pop(0) + + default_iface = None + gateway = None + while route_table: + line = route_table.pop(0) + iface, destination, gateway = line.split('\t')[0:3] + if destination == '00000000': + default_iface = iface + break + + if not default_iface: + raise exceptions.NoDefaultInterfaceFoundError + + if default_iface not in netifaces.interfaces(): + raise exceptions.InterfaceNotFoundError + + return default_iface, gateway + + def ping_gateway(self, gateway): + # TODO: Discuss how much packet loss (%) is acceptable. + + # XXX -- validate gateway + # -- is it a valid ip? (there's something in util) + # -- is it a domain? + # -- can we resolve? -- raise NoDNSError if not. + packet_loss = ping.quiet_ping(gateway)[0] + if packet_loss > constants.MAX_ICMP_PACKET_LOSS: + raise exceptions.NoConnectionToGateway + + # XXX check for name resolution servers + # dunno what's the best way to do this... + # check for etc/resolv entries or similar? + # just try to resolve? + # is there something in psutil? + + # def check_name_resolution(self): + # pass diff --git a/src/leap/base/constants.py b/src/leap/base/constants.py index 48a18dc3..f7be8d98 100644 --- a/src/leap/base/constants.py +++ b/src/leap/base/constants.py @@ -28,3 +28,5 @@ DEFAULT_PROVIDER_DEFINITION = { u'version': u'0.1.0'} MAX_ICMP_PACKET_LOSS = 10 + +ROUTE_CHECK_INTERVAL = 10 diff --git a/src/leap/base/exceptions.py b/src/leap/base/exceptions.py index 9c4aa77b..f12a49d5 100644 --- a/src/leap/base/exceptions.py +++ b/src/leap/base/exceptions.py @@ -1,6 +1,72 @@ +""" +Exception attributes and their meaning/uses +------------------------------------------- + +* critical: if True, will abort execution prematurely, + after attempting any cleaning + action. + +* failfirst: breaks any error_check loop that is examining + the error queue. + +* message: the message that will be used in the __repr__ of the exception. + +* usermessage: the message that will be passed to user in ErrorDialogs + in Qt-land. +""" + + +class LeapException(Exception): + """ + base LeapClient exception + sets some parameters that we will check + during error checking routines + """ + critical = False + failfirst = False + warning = False + + +class CriticalError(LeapException): + """ + we cannot do anything about it + """ + critical = True + failfirst = True + + +# In use ??? +# don't thing so. purge if not... + class MissingConfigFileError(Exception): pass class ImproperlyConfigured(Exception): pass + + +class NoDefaultInterfaceFoundError(LeapException): + message = "no default interface found" + usermessage = "Looks like your computer is not connected to the internet" + + +class InterfaceNotFoundError(LeapException): + # XXX should take iface arg on init maybe? + message = "interface not found" + + +class NoConnectionToGateway(CriticalError): + message = "no connection to gateway" + usermessage = "Looks like there are problems with your internet connection" + + +class NoInternetConnection(CriticalError): + message = "No Internet connection found" + usermessage = "It looks like there is no internet connection." + # and now we try to connect to our web to troubleshoot LOL :P + + +class TunnelNotDefaultRouteError(CriticalError): + message = "Tunnel connection dissapeared. VPN down?" + usermessage = "The Encrypted Connection was lost. Shutting down..." diff --git a/src/leap/base/network.py b/src/leap/base/network.py new file mode 100644 index 00000000..e90139c4 --- /dev/null +++ b/src/leap/base/network.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +from __future__ import (print_function) +import logging +import threading + +from leap.base.checks import LeapNetworkChecker +from leap.base.constants import ROUTE_CHECK_INTERVAL +from leap.base.exceptions import TunnelNotDefaultRouteError +from leap.util.coroutines import (launch_thread, process_events) + +from time import sleep + +logger = logging.getLogger(name=__name__) + + +class NetworkCheckerThread(object): + """ + Manages network checking thread that makes sure we have a working network + connection. + """ + def __init__(self, *args, **kwargs): + self.status_signals = kwargs.pop('status_signals', None) + #self.watcher_cb = kwargs.pop('status_signals', None) + self.error_cb = kwargs.pop( + 'error_cb', + lambda exc: logger.error("%s", exc.message)) + self.shutdown = threading.Event() + + # XXX get provider_gateway and pass it to checker + # see in eip.config for function + # #718 + self.checker = LeapNetworkChecker() + + def start(self): + self.process_handle = self._launch_recurrent_network_checks( + (self.error_cb,)) + + def stop(self): + self.shutdown.set() + logger.debug("network checked stopped.") + + def run_checks(self): + pass + + #private methods + + #here all the observers in fail_callbacks expect one positional argument, + #which is exception so we can try by passing a lambda with logger to + #check it works. + def _network_checks_thread(self, fail_callbacks): + #TODO: replace this with waiting for a signal from openvpn + while True: + try: + self.checker.check_tunnel_default_interface() + break + except TunnelNotDefaultRouteError: + # XXX ??? why do we sleep here??? + sleep(1) + + fail_observer_dict = dict((( + observer, + process_events(observer)) for observer in fail_callbacks)) + while not self.shutdown.is_set(): + try: + self.checker.check_tunnel_default_interface() + self.checker.check_internet_connection() + sleep(ROUTE_CHECK_INTERVAL) + except Exception as exc: + for obs in fail_observer_dict: + fail_observer_dict[obs].send(exc) + sleep(ROUTE_CHECK_INTERVAL) + #reset event + self.shutdown.clear() + + def _launch_recurrent_network_checks(self, fail_callbacks): + #we need to wrap the fail callback in a tuple + watcher = launch_thread( + self._network_checks_thread, + (fail_callbacks,)) + return watcher diff --git a/src/leap/base/tests/test_checks.py b/src/leap/base/tests/test_checks.py new file mode 100644 index 00000000..bec09ce6 --- /dev/null +++ b/src/leap/base/tests/test_checks.py @@ -0,0 +1,117 @@ +try: + import unittest2 as unittest +except ImportError: + import unittest +import os + +from mock import (patch, Mock) +from StringIO import StringIO + +import ping +import requests + +from leap.base import checks +from leap.base import exceptions +from leap.testing.basetest import BaseLeapTest + +_uid = os.getuid() + + +class LeapNetworkCheckTest(BaseLeapTest): + __name__ = "leap_network_check_tests" + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_checker_should_implement_check_methods(self): + checker = checks.LeapNetworkChecker() + + self.assertTrue(hasattr(checker, "check_internet_connection"), + "missing meth") + self.assertTrue(hasattr(checker, "check_tunnel_default_interface"), + "missing meth") + self.assertTrue(hasattr(checker, "is_internet_up"), + "missing meth") + self.assertTrue(hasattr(checker, "ping_gateway"), + "missing meth") + + def test_checker_should_actually_call_all_tests(self): + checker = checks.LeapNetworkChecker() + + mc = Mock() + checker.run_all(checker=mc) + self.assertTrue(mc.check_internet_connection.called, "not called") + self.assertTrue(mc.check_tunnel_default_interface.called, "not called") + self.assertTrue(mc.ping_gateway.called, "not called") + self.assertTrue(mc.is_internet_up.called, "not called") + + def test_get_default_interface_no_interface(self): + checker = checks.LeapNetworkChecker() + with patch('leap.base.checks.open', create=True) as mock_open: + with self.assertRaises(exceptions.NoDefaultInterfaceFoundError): + mock_open.return_value = StringIO( + "Iface\tDestination Gateway\t" + "Flags\tRefCntd\tUse\tMetric\t" + "Mask\tMTU\tWindow\tIRTT") + checker.get_default_interface_gateway() + + def test_check_tunnel_default_interface(self): + checker = checks.LeapNetworkChecker() + with patch('leap.base.checks.open', create=True) as mock_open: + with self.assertRaises(exceptions.TunnelNotDefaultRouteError): + mock_open.return_value = StringIO( + "Iface\tDestination Gateway\t" + "Flags\tRefCntd\tUse\tMetric\t" + "Mask\tMTU\tWindow\tIRTT") + checker.check_tunnel_default_interface() + + with patch('leap.base.checks.open', create=True) as mock_open: + with self.assertRaises(exceptions.TunnelNotDefaultRouteError): + mock_open.return_value = StringIO( + "Iface\tDestination Gateway\t" + "Flags\tRefCntd\tUse\tMetric\t" + "Mask\tMTU\tWindow\tIRTT\n" + "wlan0\t00000000\t0102A8C0\t" + "0003\t0\t0\t0\t00000000\t0\t0\t0") + checker.check_tunnel_default_interface() + + with patch('leap.base.checks.open', create=True) as mock_open: + mock_open.return_value = StringIO( + "Iface\tDestination Gateway\t" + "Flags\tRefCntd\tUse\tMetric\t" + "Mask\tMTU\tWindow\tIRTT\n" + "tun0\t00000000\t01002A0A\t0003\t0\t0\t0\t00000080\t0\t0\t0") + checker.check_tunnel_default_interface() + + def test_ping_gateway_fail(self): + checker = checks.LeapNetworkChecker() + with patch.object(ping, "quiet_ping") as mocked_ping: + with self.assertRaises(exceptions.NoConnectionToGateway): + mocked_ping.return_value = [11, "", ""] + checker.ping_gateway("4.2.2.2") + + def test_check_internet_connection_failures(self): + checker = checks.LeapNetworkChecker() + with patch.object(requests, "get") as mocked_get: + mocked_get.side_effect = requests.HTTPError + with self.assertRaises(exceptions.NoInternetConnection): + checker.check_internet_connection() + + with patch.object(requests, "get") as mocked_get: + mocked_get.side_effect = requests.RequestException + with self.assertRaises(exceptions.NoInternetConnection): + checker.check_internet_connection() + + #TODO: Mock possible errors that can be raised by is_internet_up + with patch.object(requests, "get") as mocked_get: + mocked_get.side_effect = requests.ConnectionError + with self.assertRaises(exceptions.NoInternetConnection): + checker.check_internet_connection() + + @unittest.skipUnless(_uid == 0, "root only") + def test_ping_gateway(self): + checker = checks.LeapNetworkChecker() + checker.ping_gateway("4.2.2.2") |