From 8487dcd0b9565657e1e6e89c7d8467d54a7c41ba Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 11 Aug 2017 13:17:37 -0400 Subject: [feature] allow manual gateway selection for vpn For now, the way to select a gateway is to add a section in bitmaskd.cfg: [vpn_prefs] locations = ["frankfurt", "seattle__wa"] countries = ["DE", "US"] Note that the location indication has priority over country code. This will be exposed by the UI in release 0.11 - Resolves: #8855 --- docs/changelog.rst | 3 +- docs/index.rst | 1 + docs/vpn/index.rst | 39 ++++++++++++++++++++++++++ src/leap/bitmask/cli/command.py | 2 +- src/leap/bitmask/vpn/gateways.py | 59 ++++++++++++++++++++++++++++++++++++++-- src/leap/bitmask/vpn/service.py | 20 +++++++++++--- tests/unit/vpn/test_gateways.py | 38 ++++++++++++++++++++++++++ 7 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 docs/vpn/index.rst diff --git a/docs/changelog.rst b/docs/changelog.rst index c89f51ed..6f94d873 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Features ~~~~~~~~ +- Initial cli port of the legacy vpn code - `#8112 `_: Check validity of key signature - `#8755 `_: Add account based keymanagement API - `#8770 `_: Simplify mail status in the cli @@ -16,7 +17,7 @@ Features - `#8049 `_: Restart the VPN automatically - `#8852 `_: Stop the vpn (and all services) when application is shut down - `#8804 `_: Automatic selection of gateways, based on user timezone -- Initial cli port of the legacy vpn code +- `#8855 `_: Manual override for the vpn gateway selection - Add VPN API to bitmask.js - Add vpn get_cert command - Indicate a successful/failure OpenPGP header import diff --git a/docs/index.rst b/docs/index.rst index ecd9d603..bc509ca2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -66,6 +66,7 @@ Contents hacking/index bundles/index cli/index + vpn/index core/index bonafide/index keymanager/index diff --git a/docs/vpn/index.rst b/docs/vpn/index.rst new file mode 100644 index 00000000..7bb4799a --- /dev/null +++ b/docs/vpn/index.rst @@ -0,0 +1,39 @@ +:LastChangedDate: $LastChangedDate$ +:LastChangedRevision: $LastChangedRevision$ +:LastChangedBy: $LastChangedBy$ + +.. _vpn: + + +Bitmask VPN +================================ + +The Bitmask VPN Module + +Gateway Selection +----------------------------------- + +By default, the Gateway Selector will apply a heuristic based on the configured +timezone of the system. This will choose the closest gateway based on the +timezones that the provider states in the ``eip-config.json`` file. + +If the locations section is not properly set by the provider, or if the user +wants to manually override the selection, the only way to do this for the +``0.10`` version of Bitmask is to add a section to the ``bitmaskd.cfg`` +configuration file:: + + [vpn_prefs] + locations = ["rio__br"] + countries = ["BR", "AR", "UY"] + +Take into account that the locations entry has precedence over the country codes enumeration. + +Also, the normalization is done so that any non-alphabetic character is substituted by an underscore ('``_``). + +You can list all the configured locations using the CLI:: + + % bitmaskctl vpn list + demo.bitmask.net [DE] Frankfurt (UTC+1) + demo.bitmask.net [US] Seattle, WA (UTC-7) + +This manual override functionality will be exposed through the UI and the CLI in release ``0.11``. diff --git a/src/leap/bitmask/cli/command.py b/src/leap/bitmask/cli/command.py index 1daadc5e..5586d091 100644 --- a/src/leap/bitmask/cli/command.py +++ b/src/leap/bitmask/cli/command.py @@ -54,7 +54,7 @@ def default_dict_printer(result): for key, value in result.items(): if isinstance(value, list): - if isinstance(value[0], list): + if value and isinstance(value[0], list): value = map(lambda l: ' '.join(l), value) for item in value: pprint('\t' + item) diff --git a/src/leap/bitmask/vpn/gateways.py b/src/leap/bitmask/vpn/gateways.py index 950e37c3..92367c30 100644 --- a/src/leap/bitmask/vpn/gateways.py +++ b/src/leap/bitmask/vpn/gateways.py @@ -18,7 +18,7 @@ """ Gateway Selection """ - +import copy import logging import os import re @@ -27,24 +27,49 @@ import time from leap.common.config import get_path_prefix +def _normalized(label): + return label.lower().replace(',', '_').replace(' ', '_') + + 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): + def __init__(self, gateways=None, locations=None, tz_offset=None, preferred=None): ''' Constructor for GatewaySelector. + By default, we will use a Time Zone Heuristic to choose the closest + gateway to the user. + + If the user specified something in the 'vpn_prefs' section of + bitmaskd.cfg, we will passed a dictionary here with entries for + 'countries' and 'locations', in the form of a list. The 'locations' + entry has preference over the 'countries' one. + + :param gateways: an unordered list of all the available gateways, read + from the eip-config file. + :type gateways: list + :param locations: a dictionary with the locations, as read from the + eip-config file + :type locations: dict :param tz_offset: use this offset as a local distance to GMT. :type tz_offset: int + :param preferred: a dictionary containing the country code (cc) and the + locations (locations) manually preferred by the user. + :type preferred: dict ''' if gateways is None: gateways = [] if locations is None: locations = {} + if preferred is None: + preferred = {} self.gateways = gateways self.locations = locations + self.preferred = preferred + self._local_offset = tz_offset if tz_offset is None: tz_offset = self._get_local_offset() @@ -90,7 +115,35 @@ class GatewaySelector(object): result = [] for ip, distance, label, country in gateways_timezones: result.append((label, ip, country)) - return result + + filtered = self.apply_user_preferences(result) + return filtered + + def apply_user_preferences(self, options): + """ + We re-sort the pre-sorted list of gateway options, according with the + user's preferences. + + Location has preference over the Country Code indication. + """ + applied = [] + presorted = copy.copy(options) + for location in self.preferred.get('loc', []): + for index, data in enumerate(presorted): + label, ip, country = data + if _normalized(label) == _normalized(location): + applied.append((label, ip, country)) + presorted.pop(index) + + for cc in self.preferred.get('cc', []): + for index, data in enumerate(presorted): + label, ip, country = data + if _normalized(country) == _normalized(cc): + applied.append((label, ip, country)) + presorted.pop(index) + if presorted: + applied += presorted + return applied def get_gateways_country_code(self): country_codes = {} diff --git a/src/leap/bitmask/vpn/service.py b/src/leap/bitmask/vpn/service.py index b0dcba86..d2f952ef 100644 --- a/src/leap/bitmask/vpn/service.py +++ b/src/leap/bitmask/vpn/service.py @@ -19,7 +19,7 @@ """ VPN service declaration. """ - +import json import os from time import strftime @@ -133,6 +133,7 @@ class VPNService(HookableService): return {'result': 'vpn stopped'} def do_status(self): + # TODO - add the current gateway and CC to the status childrenStatus = { 'vpn': {'status': 'off', 'error': None}, 'firewall': {'status': 'off', 'error': None}, @@ -220,10 +221,21 @@ class VPNService(HookableService): bonafide = self.parent.getServiceNamed('bonafide') config = yield bonafide.do_provider_read(provider, 'eip') - sorted_gateways = GatewaySelector( - config.gateways, config.locations).select_gateways() + try: + _cco = self.parent.get_config('vpn_prefs', 'countries', "") + pref_cco = json.loads(_cco) + except ValueError: + pref_cco = [] + try: + _loc = self.parent.get_config('vpn_prefs', 'locations', "") + pref_loc = json.loads(_loc) + except ValueError: + pref_loc = [] - # TODO - add manual gateway selection ability. + sorted_gateways = GatewaySelector( + config.gateways, config.locations, + preferred={'cc': pref_cco, 'loc': pref_loc} + ).select_gateways() extra_flags = config.openvpn_configuration diff --git a/tests/unit/vpn/test_gateways.py b/tests/unit/vpn/test_gateways.py index cc7fbca3..58b26761 100644 --- a/tests/unit/vpn/test_gateways.py +++ b/tests/unit/vpn/test_gateways.py @@ -126,3 +126,41 @@ class GatewaySelectorTestCase(unittest.TestCase): 14) gateways = selector.select_gateways() assert gateways == [ips[4], ips[2], ips[3], ips[1]] + + def test_apply_user_preferences(self): + preferred = { + 'loc': ['anarres', 'paris__fr', 'montevideo'], + 'cc': ['BR', 'AR', 'UY'], + } + selector = GatewaySelector(preferred=preferred) + pre = [ + ('Seattle', '1.1.1.1', 'US'), + ('Rio de Janeiro', '1.1.1.1', 'BR'), + ('Montevideo', '1.1.1.1', 'UY'), + ('Cordoba', '1.1.1.1', 'AR')] + ordered = selector.apply_user_preferences(pre) + locations = [x[0] for x in ordered] + # first the preferred location, then order by country + assert locations == ['Montevideo', 'Rio de Janeiro', 'Cordoba', 'Seattle'] + + pre = [ + ('Seattle', '', ''), + ('Montevideo', '', ''), + ('Paris, FR', '', ''), + ('AnaRreS', '', '')] + ordered = selector.apply_user_preferences(pre) + locations = [x[0] for x in ordered] + # first the preferred location, then order by country (test normalization) + assert locations == ['AnaRreS', 'Paris, FR', 'Montevideo', 'Seattle'] + + pre = [ + ('Rio De Janeiro', '', 'BR'), + ('Tacuarembo', '', 'UY'), + ('Sao Paulo', '', 'BR'), + ('Cordoba', '', 'AR')] + ordered = selector.apply_user_preferences(pre) + locations = [x[0] for x in ordered] + # no matching location, order by country + assert locations == ['Rio De Janeiro', 'Sao Paulo', 'Cordoba', 'Tacuarembo'] + + -- cgit v1.2.3