From d64f3c22c132c5de0d759d1e76ff7ced054bfcaa Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 11 Aug 2017 00:59:56 +0200 Subject: [feature] automatic vpn gateway selection, based on timezone This is a first approach to automatic gateways selection. More things are missing: - allow manual selection, by location or country code. - take the hemisphere into account. - expose the selected gw to the api/cli but overall seems this is a good approach to make 0.10 release usable in terms of vpn. - Resolves: #8804 --- src/leap/bitmask/vpn/gateways.py | 143 +++++++++++++++++++++++++++++++++++++++ src/leap/bitmask/vpn/launcher.py | 51 -------------- src/leap/bitmask/vpn/service.py | 21 +++--- 3 files changed, 156 insertions(+), 59 deletions(-) create mode 100644 src/leap/bitmask/vpn/gateways.py (limited to 'src/leap/bitmask/vpn') diff --git a/src/leap/bitmask/vpn/gateways.py b/src/leap/bitmask/vpn/gateways.py new file mode 100644 index 0000000..950e37c --- /dev/null +++ b/src/leap/bitmask/vpn/gateways.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# gateways.py +# Copyright (C) 2013-2017 LEAP Encryption Access Project +# +# 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 . + +""" +Gateway Selection +""" + +import logging +import os +import re +import time + +from leap.common.config import get_path_prefix + + +class GatewaySelector(object): + + # http://www.timeanddate.com/time/map/ + equivalent_timezones = {13: -11, 14: -10} + + def __init__(self, gateways=None, locations=None, tz_offset=None): + ''' + Constructor for GatewaySelector. + + :param tz_offset: use this offset as a local distance to GMT. + :type tz_offset: int + ''' + if gateways is None: + gateways = [] + if locations is None: + locations = {} + self.gateways = gateways + self.locations = locations + self._local_offset = tz_offset + if tz_offset is None: + tz_offset = self._get_local_offset() + if tz_offset in self.equivalent_timezones: + tz_offset = self.equivalent_timezones[tz_offset] + self._local_offset = tz_offset + + def select_gateways(self): + """ + Returns the IPs top 4 preferred gateways, in order. + """ + gateways = [gateway[1] for gateway in self.get_sorted_gateways()][:4] + return gateways + + def get_sorted_gateways(self): + """ + Returns a tuple with location-label, IP and Country Code with all the + available gateways, sorted by order of preference. + """ + gateways_timezones = [] + locations = self.locations + + for idx, gateway in enumerate(self.gateways): + distance = 99 # if hasn't location -> should go last + location = locations.get(gateway.get('location')) + + label = gateway.get('location', 'Unknown') + country = 'XX' + if location is not None: + country = location.get('country_code', 'XX') + label = location.get('name', label) + timezone = location.get('timezone') + if timezone is not None: + offset = int(timezone) + if offset in self.equivalent_timezones: + offset = self.equivalent_timezones[offset] + distance = self._get_timezone_distance(offset) + ip = self.gateways[idx].get('ip_address') + gateways_timezones.append((ip, distance, label, country)) + + gateways_timezones = sorted(gateways_timezones, key=lambda gw: gw[1]) + + result = [] + for ip, distance, label, country in gateways_timezones: + result.append((label, ip, country)) + return result + + def get_gateways_country_code(self): + country_codes = {} + locations = self.locations + if not locations: + return + gateways = self.gateways + + for idx, gateway in enumerate(gateways): + gateway_location = gateway.get('location') + + ip = self._eipconfig.get_gateway_ip(idx) + if gateway_location is not None: + ccode = locations[gateway['location']]['country_code'] + country_codes[ip] = ccode + return country_codes + + def _get_timezone_distance(self, offset): + ''' + Return the distance between the local timezone and + the one with offset 'offset'. + + :param offset: the distance of a timezone to GMT. + :type offset: int + :returns: distance between local offset and param offset. + :rtype: int + ''' + timezones = range(-11, 13) + tz1 = offset + tz2 = self._local_offset + distance = abs(timezones.index(tz1) - timezones.index(tz2)) + if distance > 12: + if tz1 < 0: + distance = timezones.index(tz1) + timezones[::-1].index(tz2) + else: + distance = timezones[::-1].index(tz1) + timezones.index(tz2) + + return distance + + def _get_local_offset(self): + ''' + Return the distance between GMT and the local timezone. + + :rtype: int + ''' + local_offset = time.timezone + if time.daylight: + local_offset = time.altzone + + return -local_offset / 3600 diff --git a/src/leap/bitmask/vpn/launcher.py b/src/leap/bitmask/vpn/launcher.py index 466f6d8..5f4881c 100644 --- a/src/leap/bitmask/vpn/launcher.py +++ b/src/leap/bitmask/vpn/launcher.py @@ -109,57 +109,6 @@ class VPNLauncher(object): PREFERRED_PORTS = ("443", "80", "53", "1194") - # FIXME -- dead code? - @classmethod - @abstractmethod - def get_gateways(kls, vpnconfig, providerconfig): - """ - Return a list with the selected gateways for a given provider, looking - at the VPN config file. - Each item of the list is a tuple containing (gateway, port). - - :param vpnconfig: vpn configuration object - :type vpnconfig: VPNConfig - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :rtype: list - """ - gateways = [] - - settings = Settings() - domain = providerconfig.get_domain() - gateway_conf = settings.get_selected_gateway(domain) - gateway_selector = VPNGatewaySelector(vpnconfig) - - if gateway_conf == GATEWAY_AUTOMATIC: - gws = gateway_selector.get_gateways() - else: - gws = [gateway_conf] - - if not gws: - log.error('No gateway was found!') - raise VPNLauncherException('No gateway was found!') - - for idx, gw in enumerate(gws): - ports = vpnconfig.get_gateway_ports(idx) - - the_port = "1194" # default port - - # pick the port preferring this order: - for port in kls.PREFERRED_PORTS: - if port in ports: - the_port = port - break - else: - continue - - gateways.append((gw, the_port)) - - log.debug('Using gateways (ip, port): {0!r}'.format(gateways)) - return gateways - @classmethod @abstractmethod def get_vpn_command(kls, vpnconfig, providerconfig, diff --git a/src/leap/bitmask/vpn/service.py b/src/leap/bitmask/vpn/service.py index 93080fe..8bcee2e 100644 --- a/src/leap/bitmask/vpn/service.py +++ b/src/leap/bitmask/vpn/service.py @@ -28,6 +28,7 @@ from twisted.logger import Logger from leap.bitmask.hooks import HookableService from leap.bitmask.util import merge_status +from leap.bitmask.vpn.gateways import GatewaySelector from leap.bitmask.vpn.fw.firewall import FirewallManager from leap.bitmask.vpn.tunnel import TunnelManager from leap.bitmask.vpn._checks import is_service_ready, get_vpn_cert_path @@ -198,14 +199,15 @@ class VPNService(HookableService): bonafide = self.parent.getServiceNamed("bonafide") config = yield bonafide.do_provider_read(provider, "eip") - # TODO - add gateway selection ability. - # First thing, we should port the TimezonSelector - remotes = [(gw["ip_address"], gw["capabilities"]["ports"][0]) - for gw in config.gateways] + sorted_gateways = GatewaySelector( + config.gateways, config.locations).select_gateways() + + # TODO - add manual gateway selection ability. + extra_flags = config.openvpn_configuration - prefix = os.path.join(self._basepath, "leap", "providers", provider, - "keys") + prefix = os.path.join( + self._basepath, "leap", "providers", provider, "keys") cert_path = key_path = os.path.join(prefix, "client", "openvpn.pem") ca_path = os.path.join(prefix, "ca", "cacert.pem") @@ -217,13 +219,16 @@ class VPNService(HookableService): 'Cannot find provider certificate. ' 'Please configure provider.') + # TODO add remote ports, according to preferred sequence + remotes = tuple([(ip, '443') for ip in sorted_gateways]) self._tunnel = TunnelManager( provider, remotes, cert_path, key_path, ca_path, extra_flags) self._firewall = FirewallManager(remotes) def _cert_expires(self, provider): - path = os.path.join(self._basepath, "leap", "providers", provider, - "keys", "client", "openvpn.pem") + path = os.path.join( + self._basepath, "leap", "providers", provider, + "keys", "client", "openvpn.pem") with open(path, 'r') as f: cert = f.read() _, to = get_cert_time_boundaries(cert) -- cgit v1.2.3