# -*- coding: utf-8 -*- # launcher.py # Copyright (C) 2013-2017 LEAP # # 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 . """ Platform-independent VPN launcher interface. """ import getpass import hashlib import os import stat from twisted.logger import Logger from abc import ABCMeta, abstractmethod from functools import partial from leap.bitmask.vpn.constants import IS_LINUX, IS_MAC from leap.bitmask.vpn.utils import force_eval log = Logger() flags_STANDALONE = False class VPNLauncherException(Exception): pass class OpenVPNNotFoundException(VPNLauncherException): pass def _has_updown_scripts(path, warn=True): """ Checks the existence of the up/down scripts and its exec bit if applicable. :param path: the path to be checked :type path: str :param warn: whether we should log the absence :type warn: bool :rtype: bool """ is_file = os.path.isfile(path) if warn and not is_file: log.error('Could not find up/down script %s. ' 'Might produce DNS leaks.' % (path,)) # XXX check if applies in win is_exe = False try: is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) except OSError as e: log.warn("%s" % (e,)) if warn and not is_exe: log.error('Up/down script %s is not executable. ' 'Might produce DNS leaks.' % (path,)) return is_file and is_exe def _has_other_files(path, warn=True): """ Check the existence of other important files. :param path: the path to be checked :type path: str :param warn: whether we should log the absence :type warn: bool :rtype: bool """ is_file = os.path.isfile(path) if warn and not is_file: log.warn('Could not find file during checks: %s. ' % ( path,)) return is_file class VPNLauncher(object): """ Abstract launcher class """ __metaclass__ = ABCMeta UPDOWN_FILES = None OTHER_FILES = None UP_SCRIPT = None DOWN_SCRIPT = None 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, socket_host, socket_port, remotes, openvpn_verb=1): """ Return the platform-dependant vpn command for launching openvpn. Might raise: OpenVPNNotFoundException, VPNLauncherException. :param vpnconfig: vpn configuration object :type vpnconfig: VPNConfig :param providerconfig: provider specific configuration :type providerconfig: ProviderConfig :param socket_host: either socket path (unix) or socket IP :type socket_host: str :param socket_port: either string "unix" if it's a unix socket, or port otherwise :type socket_port: str :param openvpn_verb: the openvpn verbosity wanted :type openvpn_verb: int :return: A VPN command ready to be launched. :rtype: list """ openvpn_path = force_eval(kls.OPENVPN_BIN_PATH) if not os.path.isfile(openvpn_path): log.warn('Could not find openvpn bin in path %s' % ( openvpn_path)) err = OpenVPNNotFoundException() err.expected = True raise err args = [] args += [ '--nobind' ] if openvpn_verb is not None: args += ['--verb', '%d' % (openvpn_verb,)] gateways = remotes for ip, port in gateways: args += ['--remote', ip, port, 'udp'] args += [ '--client', '--tls-client', '--remote-cert-tls', 'server', ] openvpn_configuration = vpnconfig.get_openvpn_configuration() for key, value in openvpn_configuration.items(): if type(value) is bool: if value: args += ['--%s' % (key,)] else: args += ['--%s' % (key,), value] user = getpass.getuser() if socket_port == "unix": # that's always the case for linux args += [ '--management-client-user', user ] args += [ '--management-signal', '--management', socket_host, socket_port, '--script-security', '2', '--ca', providerconfig.get_ca_cert_path(), '--cert', vpnconfig.get_client_cert_path(providerconfig), '--key', vpnconfig.get_client_cert_path(providerconfig) ] if not IS_MAC: args += [ '--ping', '5', '--ping-restart', '10', '--persist-key', '--persist-local-ip', '--persist-remote-ip'] command_and_args = [openvpn_path] + args return command_and_args