summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/changelog.rst3
-rw-r--r--docs/index.rst1
-rw-r--r--docs/vpn/index.rst39
-rw-r--r--src/leap/bitmask/cli/command.py2
-rw-r--r--src/leap/bitmask/vpn/gateways.py59
-rw-r--r--src/leap/bitmask/vpn/service.py20
-rw-r--r--tests/unit/vpn/test_gateways.py38
7 files changed, 153 insertions, 9 deletions
diff --git a/docs/changelog.rst b/docs/changelog.rst
index c89f51e..6f94d87 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -5,6 +5,7 @@ Changelog
Features
~~~~~~~~
+- Initial cli port of the legacy vpn code
- `#8112 <https://0xacab.org/leap/bitmask-dev/issues/8112>`_: Check validity of key signature
- `#8755 <https://0xacab.org/leap/bitmask-dev/issues/8755>`_: Add account based keymanagement API
- `#8770 <https://0xacab.org/leap/bitmask-dev/issues/8770>`_: Simplify mail status in the cli
@@ -16,7 +17,7 @@ Features
- `#8049 <https://0xacab.org/leap/bitmask-dev/issues/8049>`_: Restart the VPN automatically
- `#8852 <https://0xacab.org/leap/bitmask-dev/issues/8852>`_: Stop the vpn (and all services) when application is shut down
- `#8804 <https://0xacab.org/leap/bitmask-dev/issues/8804>`_: Automatic selection of gateways, based on user timezone
-- Initial cli port of the legacy vpn code
+- `#8855 <https://0xacab.org/leap/bitmask-dev/issues/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 ecd9d60..bc509ca 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 0000000..7bb4799
--- /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 1daadc5..5586d09 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 950e37c..92367c3 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 b0dcba8..d2f952e 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 cc7fbca..58b2676 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']
+
+