summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/vpn
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2017-08-11 00:59:56 +0200
committerKali Kaneko <kali@leap.se>2017-08-11 14:21:57 -0400
commitd64f3c22c132c5de0d759d1e76ff7ced054bfcaa (patch)
treecf14b625d1206ccdf44769f3ee2e14985730dc0d /src/leap/bitmask/vpn
parent763f88658a4e6d12557c7931f5435ebd35548ca7 (diff)
[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
Diffstat (limited to 'src/leap/bitmask/vpn')
-rw-r--r--src/leap/bitmask/vpn/gateways.py143
-rw-r--r--src/leap/bitmask/vpn/launcher.py51
-rw-r--r--src/leap/bitmask/vpn/service.py21
3 files changed, 156 insertions, 59 deletions
diff --git a/src/leap/bitmask/vpn/gateways.py b/src/leap/bitmask/vpn/gateways.py
new file mode 100644
index 00000000..950e37c3
--- /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 <http://www.gnu.org/licenses/>.
+
+"""
+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 466f6d8a..5f4881c7 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 93080feb..8bcee2e8 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)