diff options
Diffstat (limited to 'src/leap/eip/config.py')
-rw-r--r-- | src/leap/eip/config.py | 552 |
1 files changed, 262 insertions, 290 deletions
diff --git a/src/leap/eip/config.py b/src/leap/eip/config.py index 6118c9de..917871da 100644 --- a/src/leap/eip/config.py +++ b/src/leap/eip/config.py @@ -1,186 +1,153 @@ -import ConfigParser -import grp import logging import os import platform -import socket +import re +import tempfile -from leap.util.fileutil import (which, mkdir_p, - check_and_fix_urw_only) +from leap import __branding as BRANDING +from leap import certs +from leap.util.misc import null_check +from leap.util.fileutil import (which, mkdir_p, check_and_fix_urw_only) + +from leap.base import config as baseconfig from leap.baseapp.permcheck import (is_pkexec_in_system, is_auth_agent_running) +from leap.eip import exceptions as eip_exceptions +from leap.eip import specs as eipspecs logger = logging.getLogger(name=__name__) -logger.setLevel('DEBUG') - -# XXX move exceptions to -# from leap.eip import exceptions as eip_exceptions - - -class EIPNoPkexecAvailable(Exception): - pass - +provider_ca_file = BRANDING.get('provider_ca_file', None) -class EIPNoPolkitAuthAgentAvailable(Exception): - pass +_platform = platform.system() -class EIPInitNoProviderError(Exception): - pass +class EIPConfig(baseconfig.JSONLeapConfig): + spec = eipspecs.eipconfig_spec + def _get_slug(self): + eipjsonpath = baseconfig.get_config_file( + 'eip.json') + return eipjsonpath -class EIPInitBadProviderError(Exception): - pass + def _set_slug(self, *args, **kwargs): + raise AttributeError("you cannot set slug") + slug = property(_get_slug, _set_slug) -class EIPInitNoKeyFileError(Exception): - pass +class EIPServiceConfig(baseconfig.JSONLeapConfig): + spec = eipspecs.eipservice_config_spec -class EIPInitBadKeyFilePermError(Exception): - pass + def _get_slug(self): + domain = getattr(self, 'domain', None) + if domain: + path = baseconfig.get_provider_path(domain) + else: + path = baseconfig.get_default_provider_path() + return baseconfig.get_config_file( + 'eip-service.json', folder=path) + def _set_slug(self): + raise AttributeError("you cannot set slug") -OPENVPN_CONFIG_TEMPLATE = """#Autogenerated by eip-client wizard -remote {VPN_REMOTE_HOST} {VPN_REMOTE_PORT} + slug = property(_get_slug, _set_slug) -client -dev tun -persist-tun -persist-key -proto udp -tls-client -remote-cert-tls server -cert {LEAP_EIP_KEYS} -key {LEAP_EIP_KEYS} -ca {LEAP_EIP_KEYS} -""" +def get_socket_path(): + socket_path = os.path.join( + tempfile.mkdtemp(prefix="leap-tmp"), + 'openvpn.socket') + #logger.debug('socket path: %s', socket_path) + return socket_path -def get_config_dir(): +def get_eip_gateway(eipconfig=None, eipserviceconfig=None): """ - get the base dir for all leap config - @rparam: config path - @rtype: string + return the first host in eip service config + that matches the name defined in the eip.json config + file. """ - # TODO - # check for $XDG_CONFIG_HOME var? - # get a more sensible path for win/mac - # kclair: opinion? ^^ - return os.path.expanduser( - os.path.join('~', - '.config', - 'leap')) - - -def get_config_file(filename, folder=None): + # XXX eventually we should move to a more clever + # gateway selection. maybe we could return + # all gateways that match our cluster. + + null_check(eipconfig, "eipconfig") + null_check(eipserviceconfig, "eipserviceconfig") + PLACEHOLDER = "testprovider.example.org" + + conf = eipconfig.config + eipsconf = eipserviceconfig.config + + primary_gateway = conf.get('primary_gateway', None) + if not primary_gateway: + return PLACEHOLDER + + gateways = eipsconf.get('gateways', None) + if not gateways: + logger.error('missing gateways in eip service config') + return PLACEHOLDER + + if len(gateways) > 0: + for gw in gateways: + clustername = gw.get('cluster', None) + if not clustername: + logger.error('no cluster name') + return + + if clustername == primary_gateway: + # XXX at some moment, we must + # make this a more generic function, + # and return ports, protocols... + ipaddress = gw.get('ip_address', None) + if not ipaddress: + logger.error('no ip_address') + return + return ipaddress + logger.error('could not find primary gateway in provider' + 'gateway list') + + +def get_cipher_options(eipserviceconfig=None): """ - concatenates the given filename - with leap config dir. - @param filename: name of the file - @type filename: string - @rparam: full path to config file + gathers optional cipher options from eip-service config. + :param eipserviceconfig: EIPServiceConfig instance """ - path = [] - path.append(get_config_dir()) - if folder is not None: - path.append(folder) - path.append(filename) - return os.path.join(*path) + null_check(eipserviceconfig, 'eipserviceconfig') + eipsconf = eipserviceconfig.get_config() + ALLOWED_KEYS = ("auth", "cipher", "tls-cipher") + CIPHERS_REGEX = re.compile("[A-Z0-9\-]+") + opts = [] + if 'openvpn_configuration' in eipsconf: + config = eipserviceconfig.config.get( + "openvpn_configuration", {}) + for key, value in config.items(): + if key in ALLOWED_KEYS and value is not None: + sanitized_val = CIPHERS_REGEX.findall(value) + if len(sanitized_val) != 0: + _val = sanitized_val[0] + opts.append('--%s' % key) + opts.append('%s' % _val) + return opts -def get_default_provider_path(): - default_subpath = os.path.join("providers", - "default") - default_provider_path = get_config_file( - '', - folder=default_subpath) - return default_provider_path +LINUX_UP_DOWN_SCRIPT = "/etc/leap/resolv-update" +OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" -def validate_ip(ip_str): +def has_updown_scripts(): """ - raises exception if the ip_str is - not a valid representation of an ip + checks the existence of the up/down scripts """ - socket.inet_aton(ip_str) + # XXX should check permissions too + is_file = os.path.isfile(LINUX_UP_DOWN_SCRIPT) + if not is_file: + logger.warning( + "Could not find up/down scripts at %s! " + "Risk of DNS Leaks!!!") + return is_file -def check_or_create_default_vpnconf(config): - """ - checks that a vpn config file - exists for a default provider, - or creates one if it does not. - ATM REQURES A [provider] section in - eip.cfg with _at least_ a remote_ip value - """ - default_provider_path = get_default_provider_path() - - if not os.path.isdir(default_provider_path): - mkdir_p(default_provider_path) - - conf_file = get_config_file( - 'openvpn.conf', - folder=default_provider_path) - - if os.path.isfile(conf_file): - return - else: - logger.debug( - 'missing default openvpn config\n' - 'creating one...') - - # We're getting provider from eip.cfg - # by now. Get it from a list of gateways - # instead. - - try: - remote_ip = config.get('provider', - 'remote_ip') - validate_ip(remote_ip) - - except ConfigParser.NoOptionError: - raise EIPInitNoProviderError - - except socket.error: - # this does not look like an ip, dave - raise EIPInitBadProviderError - - if config.has_option('provider', 'remote_port'): - remote_port = config.get('provider', - 'remote_port') - else: - remote_port = 1194 - - default_subpath = os.path.join("providers", - "default") - default_provider_path = get_config_file( - '', - folder=default_subpath) - - if not os.path.isdir(default_provider_path): - mkdir_p(default_provider_path) - - conf_file = get_config_file( - 'openvpn.conf', - folder=default_provider_path) - - # XXX keys have to be manually placed by now - keys_file = get_config_file( - 'openvpn.keys', - folder=default_provider_path) - - ovpn_config = OPENVPN_CONFIG_TEMPLATE.format( - VPN_REMOTE_HOST=remote_ip, - VPN_REMOTE_PORT=remote_port, - LEAP_EIP_KEYS=keys_file) - - with open(conf_file, 'wb') as f: - f.write(ovpn_config) - - -def build_ovpn_options(daemon=False): +def build_ovpn_options(daemon=False, socket_path=None, **kwargs): """ build a list of options to be passed in the @@ -195,17 +162,57 @@ def build_ovpn_options(daemon=False): # since we will need to take some # things from there if present. + provider = kwargs.pop('provider', None) + eipconfig = EIPConfig(domain=provider) + eipconfig.load() + eipserviceconfig = EIPServiceConfig(domain=provider) + eipserviceconfig.load() + # get user/group name # also from config. - user = os.getlogin() - gid = os.getgroups()[-1] - group = grp.getgrgid(gid).gr_name + user = baseconfig.get_username() + group = baseconfig.get_groupname() opts = [] - #moved to config files - #opts.append('--persist-tun') - #opts.append('--persist-key') + opts.append('--client') + + opts.append('--dev') + # XXX same in win? + opts.append('tun') + opts.append('--persist-tun') + opts.append('--persist-key') + + verbosity = kwargs.get('ovpn_verbosity', None) + if verbosity and 1 <= verbosity <= 6: + opts.append('--verb') + opts.append("%s" % verbosity) + + # remote ############################## + # (server, port, protocol) + + opts.append('--remote') + + gw = get_eip_gateway(eipconfig=eipconfig, + eipserviceconfig=eipserviceconfig) + logger.debug('setting eip gateway to %s', gw) + opts.append(str(gw)) + + # get port/protocol from eipservice too + opts.append('1194') + #opts.append('80') + opts.append('udp') + + opts.append('--tls-client') + opts.append('--remote-cert-tls') + opts.append('server') + + # get ciphers ####################### + + ciphers = get_cipher_options( + eipserviceconfig=eipserviceconfig) + for cipheropt in ciphers: + opts.append(str(cipheropt)) # set user and group opts.append('--user') @@ -221,30 +228,44 @@ def build_ovpn_options(daemon=False): # interface. unix sockets or telnet interface for win. # XXX take them from the config object. - ourplatform = platform.system() - if ourplatform in ("Linux", "Mac"): - opts.append('--management') - opts.append('/tmp/.eip.sock') - opts.append('unix') - if ourplatform == "Windows": + if _platform == "Windows": opts.append('--management') opts.append('localhost') # XXX which is a good choice? opts.append('7777') - # remaining config options will go in a file - - # NOTE: we will build this file from - # the service definition file. - # XXX override from --with-openvpn-config + if _platform in ("Linux", "Darwin"): + opts.append('--management') - opts.append('--config') + if socket_path is None: + socket_path = get_socket_path() + opts.append(socket_path) + opts.append('unix') - default_provider_path = get_default_provider_path() - ovpncnf = get_config_file( - 'openvpn.conf', - folder=default_provider_path) - opts.append(ovpncnf) + opts.append('--script-security') + opts.append('2') + + if _platform == "Linux": + if has_updown_scripts(): + opts.append("--up") + opts.append(LINUX_UP_DOWN_SCRIPT) + opts.append("--down") + opts.append(LINUX_UP_DOWN_SCRIPT) + opts.append("--plugin") + opts.append(OPENVPN_DOWN_ROOT) + opts.append("'script_type=down %s'" % LINUX_UP_DOWN_SCRIPT) + + # certs + client_cert_path = eipspecs.client_cert_path(provider) + ca_cert_path = eipspecs.provider_ca_path(provider) + + # XXX FIX paths for MAC + opts.append('--cert') + opts.append(client_cert_path) + opts.append('--key') + opts.append(client_cert_path) + opts.append('--ca') + opts.append(ca_cert_path) # we cannot run in daemon mode # with the current subp setting. @@ -252,17 +273,16 @@ def build_ovpn_options(daemon=False): #if daemon is True: #opts.append('--daemon') + logger.debug('vpn options: %s', ' '.join(opts)) return opts -def build_ovpn_command(config, debug=False): +def build_ovpn_command(debug=False, do_pkexec_check=True, vpnbin=None, + socket_path=None, **kwargs): """ build a string with the complete openvpn invocation - @param config: config object - @type config: ConfigParser instance - @rtype [string, [list of strings]] @rparam: a list containing the command string and a list of options. @@ -271,18 +291,18 @@ def build_ovpn_command(config, debug=False): use_pkexec = True ovpn = None - if config.has_option('openvpn', 'use_pkexec'): - use_pkexec = config.get('openvpn', 'use_pkexec') - if platform.system() == "Linux" and use_pkexec: + # XXX get use_pkexec from config instead. + + if _platform == "Linux" and use_pkexec and do_pkexec_check: - # XXX check for both pkexec (done) + # check for both pkexec # AND a suitable authentication # agent running. logger.info('use_pkexec set to True') if not is_pkexec_in_system(): logger.error('no pkexec in system') - raise EIPNoPkexecAvailable + raise eip_exceptions.EIPNoPkexecAvailable if not is_auth_agent_running(): logger.warning( @@ -290,101 +310,48 @@ def build_ovpn_command(config, debug=False): "pkexec will use its own text " "based authentication agent. " "that's probably a bad idea") - raise EIPNoPolkitAuthAgentAvailable + raise eip_exceptions.EIPNoPolkitAuthAgentAvailable command.append('pkexec') - if config.has_option('openvpn', - 'openvpn_binary'): - ovpn = config.get('openvpn', - 'openvpn_binary') - if not ovpn and config.has_option('DEFAULT', - 'openvpn_binary'): - ovpn = config.get('DEFAULT', - 'openvpn_binary') - + if vpnbin is None: + if _platform == "Darwin": + # XXX Should hardcode our installed path + # /Applications/LEAPClient.app/Contents/Resources/openvpn.leap + openvpn_bin = "openvpn.leap" + else: + openvpn_bin = "openvpn" + #XXX hardcode for darwin + ovpn = which(openvpn_bin) + else: + ovpn = vpnbin if ovpn: - command.append(ovpn) - + vpn_command = ovpn + else: + vpn_command = "openvpn" + command.append(vpn_command) daemon_mode = not debug - for opt in build_ovpn_options(daemon=daemon_mode): + for opt in build_ovpn_options(daemon=daemon_mode, socket_path=socket_path, + **kwargs): command.append(opt) # XXX check len and raise proper error - return [command[0], command[1:]] - - -def get_sensible_defaults(): - """ - gathers a dict of sensible defaults, - platform sensitive, - to be used to initialize the config parser - @rtype: dict - @rparam: default options. - """ - - # this way we're passing a simple dict - # that will initialize the configparser - # and will get written to "DEFAULTS" section, - # which is fine for now. - # if we want to write to a particular section - # we can better pass a tuple of triples - # (('section1', 'foo', '23'),) - # and config.set them - - defaults = dict() - defaults['openvpn_binary'] = which('openvpn') - defaults['autostart'] = 'true' - - # TODO - # - management. - return defaults - - -def get_config(config_file=None): - """ - temporary method for getting configs, - mainly for early stage development process. - in the future we will get preferences - from the storage api - - @rtype: ConfigParser instance - @rparam: a config object - """ - # TODO - # - refactor out common things and get - # them to util/ or baseapp/ - - defaults = get_sensible_defaults() - config = ConfigParser.ConfigParser(defaults) - - if not config_file: - fpath = get_config_file('eip.cfg') - if not os.path.isfile(fpath): - dpath, cfile = os.path.split(fpath) - if not os.path.isdir(dpath): - mkdir_p(dpath) - with open(fpath, 'wb') as configfile: - config.write(configfile) - config_file = open(fpath) - - #TODO - # - convert config_file to list; - # look in places like /etc/leap/eip.cfg - # for global settings. - # - raise warnings/error if bad options. - - # at this point, the file should exist. - # errors would have been raised above. - - config.readfp(config_file) - - return config + if _platform == "Darwin": + OSX_ASADMIN = 'do shell script "%s" with administrator privileges' + # XXX fix workaround for Nones + _command = [x if x else " " for x in command] + # XXX debugging! + # XXX get openvpn log path from debug flags + _command.append('--log') + _command.append('/tmp/leap_openvpn.log') + return ["osascript", ["-e", OSX_ASADMIN % ' '.join(_command)]] + else: + return [command[0], command[1:]] -def check_vpn_keys(config): +def check_vpn_keys(provider=None): """ performs an existance and permission check over the openvpn keys file. @@ -392,35 +359,40 @@ def check_vpn_keys(config): per provider, containing the CA cert, the provider key, and our client certificate """ + assert provider is not None + provider_ca = eipspecs.provider_ca_path(provider) + client_cert = eipspecs.client_cert_path(provider) - keyopt = ('provider', 'keyfile') - - # XXX at some point, - # should separate between CA, provider cert - # and our certificate. - # make changes in the default provider template - # accordingly. - - # get vpn keys - if config.has_option(*keyopt): - keyfile = config.get(*keyopt) - else: - keyfile = get_config_file( - 'openvpn.keys', - folder=get_default_provider_path()) - logger.debug('keyfile = %s', keyfile) + logger.debug('provider ca = %s', provider_ca) + logger.debug('client cert = %s', client_cert) # if no keys, raise error. - # should be catched by the ui and signal user. + # it's catched by the ui and signal user. + + if not os.path.isfile(provider_ca): + # not there. let's try to copy. + folder, filename = os.path.split(provider_ca) + if not os.path.isdir(folder): + mkdir_p(folder) + if provider_ca_file: + cacert = certs.where(provider_ca_file) + with open(provider_ca, 'w') as pca: + with open(cacert, 'r') as cac: + pca.write(cac.read()) + + if not os.path.isfile(provider_ca): + logger.error('key file %s not found. aborting.', + provider_ca) + raise eip_exceptions.EIPInitNoKeyFileError - if not os.path.isfile(keyfile): + if not os.path.isfile(client_cert): logger.error('key file %s not found. aborting.', - keyfile) - raise EIPInitNoKeyFileError - - # check proper permission on keys - # bad perms? try to fix them - try: - check_and_fix_urw_only(keyfile) - except OSError: - raise EIPInitBadKeyFilePermError + client_cert) + raise eip_exceptions.EIPInitNoKeyFileError + + for keyfile in (provider_ca, client_cert): + # bad perms? try to fix them + try: + check_and_fix_urw_only(keyfile) + except OSError: + raise eip_exceptions.EIPInitBadKeyFilePermError |