# -*- coding: utf-8 -*- # vpnlauncher.py # Copyright (C) 2013 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 independant VPN launcher interface. """ import getpass import logging import os import stat from abc import ABCMeta, abstractmethod from functools import partial from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector from leap.bitmask.util import first from leap.bitmask.util import get_path_prefix from leap.common.check import leap_assert, leap_assert_type from leap.common.files import which logger = logging.getLogger(__name__) 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: logger.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: logger.warn("%s" % (e,)) if warn and not is_exe: logger.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): """ Checks 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: logger.warning("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 @classmethod @abstractmethod def get_vpn_command(kls, eipconfig, providerconfig, socket_host, socket_port, openvpn_verb=1): """ Returns the platform dependant vpn launching command Might raise: OpenVPNNotFoundException, VPNLauncherException. :param eipconfig: eip configuration object :type eipconfig: EIPConfig :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 """ leap_assert_type(eipconfig, EIPConfig) leap_assert_type(providerconfig, ProviderConfig) kwargs = {} if flags.STANDALONE: kwargs['path_extension'] = os.path.join( get_path_prefix(), "..", "apps", "eip") openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() openvpn = first(openvpn_possibilities) args = [] args += [ '--setenv', "LEAPOPENVPN", "1" ] if openvpn_verb is not None: args += ['--verb', '%d' % (openvpn_verb,)] gateways = [] leap_settings = LeapSettings() domain = providerconfig.get_domain() gateway_conf = leap_settings.get_selected_gateway(domain) if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: gateway_selector = VPNGatewaySelector(eipconfig) gateways = gateway_selector.get_gateways() else: gateways = [gateway_conf] if not gateways: logger.error('No gateway was found!') raise VPNLauncherException(kls.tr('No gateway was found!')) logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) for gw in gateways: args += ['--remote', gw, '1194', 'udp'] args += [ '--client', '--dev', 'tun', ############################################################## # persist-tun makes ping-restart fail because it leaves a # broken routing table ############################################################## # '--persist-tun', '--persist-key', '--tls-client', '--remote-cert-tls', 'server' ] openvpn_configuration = eipconfig.get_openvpn_configuration() for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] user = getpass.getuser() ############################################################## # The down-root plugin fails in some situations, so we don't # drop privs for the time being ############################################################## # args += [ # '--user', user, # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name # ] 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' ] if _has_updown_scripts(kls.UP_SCRIPT): args += [ '--up', '\"%s\"' % (kls.UP_SCRIPT,), ] if _has_updown_scripts(kls.DOWN_SCRIPT): args += [ '--down', '\"%s\"' % (kls.DOWN_SCRIPT,) ] ########################################################### # For the time being we are disabling the usage of the # down-root plugin, because it doesn't quite work as # expected (i.e. it doesn't run route -del as root # when finishing, so it fails to properly # restart/quit) ########################################################### # if _has_updown_scripts(kls.OPENVPN_DOWN_PLUGIN): # args += [ # '--plugin', kls.OPENVPN_DOWN_ROOT, # '\'%s\'' % kls.DOWN_SCRIPT # for OSX # '\'script_type=down %s\'' % kls.DOWN_SCRIPT # for Linux # ] args += [ '--cert', eipconfig.get_client_cert_path(providerconfig), '--key', eipconfig.get_client_cert_path(providerconfig), '--ca', providerconfig.get_ca_cert_path() ] command_and_args = [openvpn] + args logger.debug("Running VPN with command:") logger.debug(" ".join(command_and_args)) return command_and_args @classmethod def get_vpn_env(kls): """ Returns a dictionary with the custom env for the platform. This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client :rtype: dict """ return {} @classmethod def missing_updown_scripts(kls): """ Returns what updown scripts are missing. :rtype: list """ leap_assert(kls.UPDOWN_FILES is not None, "Need to define UPDOWN_FILES for this particular " "launcher before calling this method") file_exist = partial(_has_updown_scripts, warn=False) zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) missing = filter(lambda (path, exists): exists is False, zipped) return [path for path, exists in missing] @classmethod def missing_other_files(kls): """ Returns what other important files are missing during startup. Same as missing_updown_scripts but does not check for exec bit. :rtype: list """ leap_assert(kls.OTHER_FILES is not None, "Need to define OTHER_FILES for this particular " "auncher before calling this method") file_exist = partial(_has_other_files, warn=False) zipped = zip(kls.OTHER_FILES, map(file_exist, kls.OTHER_FILES)) missing = filter(lambda (path, exists): exists is False, zipped) return [path for path, exists in missing]