diff options
| -rw-r--r-- | changes/bug-5592_harcode_openvpn_path_for_bundle | 1 | ||||
| -rw-r--r-- | changes/bug-5595-increase-polkit-wait-time | 1 | ||||
| -rw-r--r-- | changes/bug-avoid-soft-restart | 1 | ||||
| -rw-r--r-- | changes/feature_linux-firewall | 1 | ||||
| -rw-r--r-- | pkg/linux/README | 4 | ||||
| -rw-r--r-- | pkg/linux/README.rst | 10 | ||||
| -rwxr-xr-x | pkg/linux/bitmask-root | 829 | ||||
| -rw-r--r-- | pkg/linux/polkit/se.leap.bitmask.policy (renamed from pkg/linux/polkit/net.openvpn.gui.leap.policy) | 12 | ||||
| -rwxr-xr-x | pkg/linux/update-resolv-conf | 58 | ||||
| -rw-r--r-- | src/leap/bitmask/backend.py | 43 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/mainwindow.py | 19 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/twisted_main.py | 1 | ||||
| -rw-r--r-- | src/leap/bitmask/platform_init/initializers.py | 9 | ||||
| -rw-r--r-- | src/leap/bitmask/services/eip/linuxvpnlauncher.py | 113 | ||||
| -rw-r--r-- | src/leap/bitmask/services/eip/vpnlauncher.py | 124 | ||||
| -rw-r--r-- | src/leap/bitmask/services/eip/vpnprocess.py | 88 | ||||
| -rw-r--r-- | src/leap/bitmask/util/privilege_policies.py | 82 | 
17 files changed, 1166 insertions, 230 deletions
diff --git a/changes/bug-5592_harcode_openvpn_path_for_bundle b/changes/bug-5592_harcode_openvpn_path_for_bundle new file mode 100644 index 00000000..67f4b533 --- /dev/null +++ b/changes/bug-5592_harcode_openvpn_path_for_bundle @@ -0,0 +1 @@ +- Hardcode paths for openvpn if STANDALONE=True. Related: #5592 diff --git a/changes/bug-5595-increase-polkit-wait-time b/changes/bug-5595-increase-polkit-wait-time new file mode 100644 index 00000000..5662e249 --- /dev/null +++ b/changes/bug-5595-increase-polkit-wait-time @@ -0,0 +1 @@ +- Increase waiting time to wait for polkit agent to be up. Closes: #5595 diff --git a/changes/bug-avoid-soft-restart b/changes/bug-avoid-soft-restart new file mode 100644 index 00000000..36795ad7 --- /dev/null +++ b/changes/bug-avoid-soft-restart @@ -0,0 +1 @@ +- Use openvpn hard restart. Closes: #5669 diff --git a/changes/feature_linux-firewall b/changes/feature_linux-firewall new file mode 100644 index 00000000..2a48da07 --- /dev/null +++ b/changes/feature_linux-firewall @@ -0,0 +1 @@ +- Use iptables firewall. Closes: #5588 diff --git a/pkg/linux/README b/pkg/linux/README deleted file mode 100644 index 7410789b..00000000 --- a/pkg/linux/README +++ /dev/null @@ -1,4 +0,0 @@ -= Files = -In GNU/Linux, we expect these files to be in place: - -resolv-update -> /etc/leap/resolv-update diff --git a/pkg/linux/README.rst b/pkg/linux/README.rst new file mode 100644 index 00000000..220565ff --- /dev/null +++ b/pkg/linux/README.rst @@ -0,0 +1,10 @@ +Files  +===== + +In GNU/Linux, we expect these files to be in place:: + + update-resolv-conf -> /etc/leap/update-resolv-conf + resolv-update -> /etc/leap/resolv-update + + bitmask-root -> /usr/sbin/bitmask-root + polkit/se.leap.bitmask.policy -> /usr/share/polkit-1/actions/se.leap.bitmask.policy diff --git a/pkg/linux/bitmask-root b/pkg/linux/bitmask-root new file mode 100755 index 00000000..136fd6a4 --- /dev/null +++ b/pkg/linux/bitmask-root @@ -0,0 +1,829 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 <http://www.gnu.org/licenses/>. +# +""" +This is a privileged helper script for safely running certain commands as root. +It should only be called by the Bitmask application. + +USAGE: +  bitmask-root firewall stop +  bitmask-root firewall start GATEWAY1 GATEWAY2 ... +  bitmask-root openvpn stop +  bitmask-root openvpn start CONFIG1 CONFIG1 ... + +All actions return exit code 0 for success, non-zero otherwise. + +The `openvpn start` action is special: it calls exec on openvpn and replaces +the current process. +""" +# TODO should be tested with python3, which can be the default on some distro. +from __future__ import print_function +import atexit +import os +import re +import signal +import socket +import subprocess +import sys +import time +import traceback + + +cmdcheck = subprocess.check_output + +## +## CONSTANTS +## + +SCRIPT = "bitmask-root" +NAMESERVER = "10.42.0.1" +BITMASK_CHAIN = "bitmask" + +IP = "/bin/ip" +IPTABLES = "/sbin/iptables" +IP6TABLES = "/sbin/ip6tables" + +RESOLVCONF_SYSTEM_BIN = "/sbin/resolvconf" +RESOLVCONF_LEAP_BIN = "/usr/local/sbin/leap-resolvconf" + +OPENVPN_USER = "nobody" +OPENVPN_GROUP = "nogroup" +LEAPOPENVPN = "LEAPOPENVPN" +OPENVPN_SYSTEM_BIN = "/usr/sbin/openvpn"  # Debian location +OPENVPN_LEAP_BIN = "/usr/sbin/leap-openvpn"  # installed by bundle + + +""" +The path to the script to update resolv.conf +""" +# XXX We have to check if we have a valid resolvconf, and use +# the old resolv-update if not. +LEAP_UPDATE_RESOLVCONF_FILE = "/etc/leap/update-resolv-conf" +LEAP_RESOLV_UPDATE = "/etc/leap/resolv-update" + +FIXED_FLAGS = [ +    "--setenv", "LEAPOPENVPN", "1", +    "--nobind", +    "--client", +    "--dev", "tun", +    "--tls-client", +    "--remote-cert-tls", "server", +    "--management-signal", +    "--script-security", "1", +    "--user", "nobody", +    "--group", "nogroup", +    "--remap-usr1", "SIGTERM", +] + +ALLOWED_FLAGS = { +    "--remote": ["IP", "NUMBER", "PROTO"], +    "--tls-cipher": ["CIPHER"], +    "--cipher": ["CIPHER"], +    "--auth": ["CIPHER"], +    "--management": ["DIR", "UNIXSOCKET"], +    "--management-client-user": ["USER"], +    "--cert": ["FILE"], +    "--key": ["FILE"], +    "--ca": ["FILE"] +} + +PARAM_FORMATS = { +    "NUMBER": lambda s: re.match("^\d+$", s), +    "PROTO": lambda s: re.match("^(tcp|udp)$", s), +    "IP": lambda s: is_valid_address(s), +    "CIPHER": lambda s: re.match("^[A-Z0-9-]+$", s), +    "USER": lambda s: re.match( +        "^[a-zA-Z0-9_\.\@][a-zA-Z0-9_\-\.\@]*\$?$", s),  # IEEE Std 1003.1-2001 +    "FILE": lambda s: os.path.isfile(s), +    "DIR": lambda s: os.path.isdir(os.path.split(s)[0]), +    "UNIXSOCKET": lambda s: s == "unix" +} + + +DEBUG = os.getenv("DEBUG") +TEST = os.getenv("TEST") + +if DEBUG: +    import logging +    formatter = logging.Formatter( +        "%(asctime)s - %(name)s - %(levelname)s - %(message)s") +    ch = logging.StreamHandler() +    ch.setLevel(logging.DEBUG) +    ch.setFormatter(formatter) +    logger = logging.getLogger(__name__) +    logger.setLevel(logging.DEBUG) +    logger.addHandler(ch) + +## +## UTILITY +## + + +def is_valid_address(value): +    """ +    Validate that the passed ip is a valid IP address. + +    :param value: the value to be validated +    :type value: str +    :rtype: bool +    """ +    try: +        socket.inet_aton(value) +        return True +    except Exception: +        print("%s: ERROR: MALFORMED IP: %s!" % (SCRIPT, value)) +        return False + + +def has_system_resolvconf(): +    """ +    Return True if resolvconf is found in the system. + +    :rtype: bool +    """ +    return os.path.isfile(RESOLVCONF) + + +def has_valid_update_resolvconf(): +    """ +    Return True if a valid update-resolv-conf script is found in the system. + +    :rtype: bool +    """ +    return os.path.isfile(LEAP_UPDATE_RESOLVCONF_FILE) + + +def has_valid_leap_resolv_update(): +    """ +    Return True if a valid resolv-update script is found in the system. + +    :rtype: bool +    """ +    return os.path.isfile(LEAP_RESOLV_UPDATE) + + +def split_list(_list, regex): +    """ +    Split a list based on a regex: +    e.g. split_list(["xx", "yy", "x1", "zz"], "^x") => [["xx", "yy"], ["x1", +    "zz"]] + +    :param _list: the list to be split. +    :type _list: list +    :param regex: the regex expression to filter with. +    :type regex: str + +    :rtype: list +    """ +    if not hasattr(regex, "match"): +        regex = re.compile(regex) +    result = [] +    i = 0 +    if not _list: +        return result +    while True: +        if regex.match(_list[i]): +            result.append([]) +            while True: +                result[-1].append(_list[i]) +                i += 1 +                if i >= len(_list) or regex.match(_list[i]): +                    break +        else: +            i += 1 +        if i >= len(_list): +            break +    return result + + +def get_process_list(): +    """ +    Get a process list by reading `/proc` filesystem. + +    :return: a list of tuples, each containing pid and command string. +    :rtype: tuple if lists +    """ +    res = [] +    pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] + +    for pid in pids: +        try: +            res.append((pid, open( +                os.path.join( +                    '/proc', pid, 'cmdline'), 'rb').read())) +        except IOError:  # proc has already terminated +            continue +    return filter(None, res) + + +class Daemon(object): +    """ +    A generic daemon class. +    """ +    def __init__(self, pidfile, stdin='/dev/null', +                 stdout='/dev/null', stderr='/dev/null'): +        self.stdin = stdin +        self.stdout = stdout +        self.stderr = stderr +        self.pidfile = pidfile + +    def daemonize(self): +        """ +        Do the UNIX double-fork magic, see Stevens' "Advanced +        Programming in the UNIX Environment" for details (ISBN 0201563177) +        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 +        """ +        try: +            pid = os.fork() +            if pid > 0: +                # exit first parent +                sys.exit(0) +        except OSError, e: +            sys.stderr.write( +                "fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) +            sys.exit(1) + +        # decouple from parent environment +        os.chdir("/") +        os.setsid() +        os.umask(0) + +        # do second fork +        try: +            pid = os.fork() +            if pid > 0: +                # exit from second parent +                sys.exit(0) +        except OSError, e: +            sys.stderr.write( +                "fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) +            sys.exit(1) + +        # redirect standard file descriptors +        sys.stdout.flush() +        sys.stderr.flush() +        si = file(self.stdin, 'r') +        so = file(self.stdout, 'a+') +        se = file(self.stderr, 'a+', 0) +        os.dup2(si.fileno(), sys.stdin.fileno()) +        os.dup2(so.fileno(), sys.stdout.fileno()) +        os.dup2(se.fileno(), sys.stderr.fileno()) + +        # write pidfile +        atexit.register(self.delpid) +        pid = str(os.getpid()) +        file(self.pidfile, 'w+').write("%s\n" % pid) + +    def delpid(self): +        """ +        Delete the pidfile. +        """ +        os.remove(self.pidfile) + +    def start(self, *args): +        """ +        Start the daemon. +        """ +        # Check for a pidfile to see if the daemon already runs +        try: +            pf = file(self.pidfile, 'r') +            pid = int(pf.read().strip()) +            pf.close() +        except IOError: +            pid = None + +        if pid: +            message = "pidfile %s already exist. Daemon already running?\n" +            sys.stderr.write(message % self.pidfile) +            sys.exit(1) + +        # Start the daemon +        self.daemonize() +        self.run(args) + +    def stop(self): +        """ +        Stop the daemon. +        """ +        # Get the pid from the pidfile +        try: +            pf = file(self.pidfile, 'r') +            pid = int(pf.read().strip()) +            pf.close() +        except IOError: +            pid = None + +        if not pid: +            message = "pidfile %s does not exist. Daemon not running?\n" +            sys.stderr.write(message % self.pidfile) +            return  # not an error in a restart + +        # Try killing the daemon process +        try: +            while 1: +                os.kill(pid, signal.SIGTERM) +                time.sleep(0.1) +        except OSError, err: +            err = str(err) +            if err.find("No such process") > 0: +                if os.path.exists(self.pidfile): +                    os.remove(self.pidfile) +            else: +                print(str(err)) +                sys.exit(1) + +    def restart(self): +        """ +        Restart the daemon. +        """ +        self.stop() +        self.start() + +    def run(self): +        """ +        This should  be overridden by derived classes. +        """ + + +def run(command, *args, **options): +    """ +    Run an external command. + +    Options: + +      `check`: If True, check the command's output. bail if non-zero. (the +               default is true unless detach or input is true) +      `exitcode`: like `check`, but return exitcode instead of bailing. +      `detach`: If True, run in detached process. +      `input`: If True, open command for writing stream to, returning the Popen +               object. +    """ +    parts = [command] +    parts.extend(args) +    if TEST or DEBUG: +        print("%s run: %s " (SCRIPT, " ".join(parts))) + +    _check = options.get("check", True) +    _detach = options.get("detach", False) +    _input = options.get("input", False) +    _exitcode = options.get("exitcode", False) + +    if not _check or _detach or _input: +        if _input: +            return subprocess.Popen(parts, stdin=subprocess.PIPE) +        else: +            # XXX ok with return None ?? +            subprocess.Popen(parts) +    else: +        try: +            devnull = open('/dev/null', 'w') +            subprocess.check_call(parts, stdout=devnull, stderr=devnull) +            return 0 +        except subprocess.CalledProcessError as exc: +            if DEBUG: +                logger.exception(exc) +            if _exitcode: +                return exc.returncode +            else: +                bail("ERROR: Could not run %s: %s" % (exc.cmd, exc.output), +                     exception=exc) + + +def bail(msg=None, exception=None): +    """ +    Abnormal exit. + +    :param msg: optional error message. +    :type msg: str +    """ +    if msg is not None: +        print("%s: %s" % (SCRIPT, msg)) +    if exception is not None: +        traceback.print_exc() +    exit(1) + +## +## OPENVPN +## + + +def get_openvpn_bin(): +    """ +    Return the path for either the system openvpn or the one the +    bundle has put there. +    """ +    if os.path.isfile(OPENVPN_SYSTEM_BIN): +        return OPENVPN_SYSTEM_BIN + +    # the bundle option should be removed from the debian package. +    if os.path.isfile(OPENVPN_LEAP_BIN): +        return OPENVPN_LEAP_BIN + + +def parse_openvpn_flags(args): +    """ +    Take argument list from the command line and parse it, only allowing some +    configuration flags. + +    :type args: list +    """ +    result = [] +    try: +        for flag in split_list(args, "^--"): +            flag_name = flag[0] +            if flag_name in ALLOWED_FLAGS: +                result.append(flag_name) +                required_params = ALLOWED_FLAGS[flag_name] +                if required_params: +                    flag_params = flag[1:] +                    if len(flag_params) != len(required_params): +                        print("%s: ERROR: not enough params for %s" % +                              (SCRIPT, flag_name)) +                        return None +                    for param, param_type in zip(flag_params, required_params): +                        if PARAM_FORMATS[param_type](param): +                            result.append(param) +                        else: +                            print("%s: ERROR: Bad argument %s" % +                                  (SCRIPT, param)) +                            return None +            else: +                print("WARNING: unrecognized openvpn flag %s" % flag_name) +        return result +    except Exception as exc: +        print("%s: ERROR PARSING FLAGS: %s" % (SCRIPT, exc)) +        if DEBUG: +            logger.exception(exc) +        return None + + +def openvpn_start(args): +    """ +    Launch openvpn, sanitizing input, and replacing the current process with +    the openvpn process. + +    :param args: arguments to be passed to openvpn +    :type args: list +    """ +    openvpn_flags = parse_openvpn_flags(args) +    if openvpn_flags: +        OPENVPN = get_openvpn_bin() +        flags = [OPENVPN] + FIXED_FLAGS + openvpn_flags +        if DEBUG: +            print("%s: running openvpn with flags:" % (SCRIPT,)) +            print(flags) +        # note: first argument to command is ignored, but customarily set to +        # the command. +        os.execv(OPENVPN, flags) +    else: +        bail('ERROR: could not parse openvpn options') + + +def openvpn_stop(args): +    """ +    Stop the openvpn that has likely been launched by bitmask. + +    :param args: arguments to openvpn +    :type args: list +    """ +    plist = get_process_list() +    OPENVPN_BIN = get_openvpn_bin() +    found_leap_openvpn = filter( +        lambda (p, s): s.startswith(OPENVPN_BIN) and LEAPOPENVPN in s, +        plist) + +    if found_leap_openvpn: +        pid = found_leap_openvpn[0][0] +        os.kill(int(pid), signal.SIGTERM) + +## +## DNS +## + + +def get_resolvconf_bin(): +    """ +    Return the path for either the system resolvconf or the one the +    bundle has put there. +    """ +    if os.path.isfile(RESOLVCONF_SYSTEM_BIN): +        return RESOLVCONF_SYSTEM_BIN + +    # the bundle option should be removed from the debian package. +    if os.path.isfile(RESOLVCONF_LEAP_BIN): +        return RESOLVCONF_LEAP_BIN + +RESOLVCONF = get_resolvconf_bin() + + +class NameserverSetter(Daemon): +    """ +    A daemon that will add leap nameserver inside the tunnel +    to the system `resolv.conf` +    """ + +    def run(self, *args): +        """ +        Run when daemonized. +        """ +        if args: +            ip_address = args[0] +            self.set_dns_nameserver(ip_address) + +    def set_dns_nameserver(self, ip_address): +        """ +        Add the tunnel DNS server to `resolv.conf` + +        :param ip_address: the ip to add to `resolv.conf` +        :type ip_address: str +        """ +        if os.path.isfile(RESOLVCONF): +            process = run(RESOLVCONF, "-a", "bitmask", input=True) +            process.communicate("nameserver %s\n" % ip_address) +        else: +            bail("ERROR: package openresolv or resolvconf not installed.") + +nameserver_setter = NameserverSetter('/tmp/leap-dns-up.pid') + + +class NameserverRestorer(Daemon): +    """ +    A daemon that will restore the previous nameservers. +    """ + +    def run(self): +        """ +        Run when daemonized. +        """ +        self.restore_dns_nameserver() + +    def restore_dns_nameserver(self): +        """ +        Remove tunnel DNS server from `resolv.conf` +        """ +        if os.path.isfile(RESOLVCONF): +            run(RESOLVCONF, "-d", "bitmask") +        else: +            print("%s: ERROR: package openresolv " +                  "or resolvconf not installed." % +                  (SCRIPT,)) + +nameserver_restorer = NameserverRestorer('/tmp/leap-dns-down.pid') + + +## +## FIREWALL +## + + +def get_gateways(gateways): +    """ +    Filter a passed sequence of gateways, returning only the valid ones. + +    :param gateways: a sequence of gateways to filter. +    :type gateways: iterable +    :rtype: iterable +    """ +    result = filter(is_valid_address, gateways) +    if not result: +        bail("ERROR: No valid gateways specified") +    else: +        return result + + +def get_default_device(): +    """ +    Retrieve the current default network device. + +    :rtype: str +    """ +    routes = subprocess.check_output([IP, "route", "show"]) +    match = re.search("^default .*dev ([^\s]*) .*$", routes, flags=re.M) +    if match.groups(): +        return match.group(1) +    else: +        bail("Could not find default device") + + +def get_local_network_ipv4(device): +    """ +    Get the local ipv4 addres for a given device. + +    :param device: +    :type device: str +    """ +    addresses = cmdcheck([IP, "-o", "address", "show", "dev", device]) +    match = re.search("^.*inet ([^ ]*) .*$", addresses, flags=re.M) +    if match.groups(): +        return match.group(1) +    else: +        return None + + +def get_local_network_ipv6(device): +    """ +    Get the local ipv6 addres for a given device. + +    :param device: +    :type device: str +    """ +    addresses = cmdcheck([IP, "-o", "address", "show", "dev", device]) +    match = re.search("^.*inet6 ([^ ]*) .*$", addresses, flags=re.M) +    if match.groups(): +        return match.group(1) +    else: +        return None + + +def run_iptable_with_check(cmd, *args, **options): +    """ +    Run an iptables command checking to see if it should: +      for --insert: run only if rule does not already exist. +      for --delete: run only if rule does exist. +    other commands are run normally. +    """ +    if "--insert" in args: +        check_args = [arg.replace("--insert", "--check") for arg in args] +        check_code = run(cmd, *check_args, exitcode=True) +        if check_code != 0: +            run(cmd, *args, **options) +    elif "--delete" in args: +        check_args = [arg.replace("--delete", "--check") for arg in args] +        check_code = run(cmd, *check_args, exitcode=True) +        if check_code == 0: +            run(cmd, *args, **options) +    else: +        run(cmd, *args, **options) + + +def iptables(*args, **options): +    """ +    Run iptables4 and iptables6. +    """ +    ip4tables(*args, **options) +    ip6tables(*args, **options) + + +def ip4tables(*args, **options): +    """ +    Run iptables4 with checks. +    """ +    run_iptable_with_check(IPTABLES, *args, **options) + + +def ip6tables(*args, **options): +    """ +    Run iptables6 with checks. +    """ +    run_iptable_with_check(IP6TABLES, *args, **options) + + +def ipv4_chain_exists(table): +    """ +    Check if a given chain exists. + +    :param table: the table to check against +    :type table: str +    :rtype: bool +    """ +    code = run(IPTABLES, "--list", table, "--numeric", exitcode=True) +    return code == 0 + + +def ipv6_chain_exists(table): +    """ +    Check if a given chain exists. + +    :param table: the table to check against +    :type table: str +    :rtype: bool +    """ +    code = run(IP6TABLES, "--list", table, "--numeric", exitcode=True) +    return code == 0 + + +def firewall_start(args): +    """ +    Bring up the firewall. + +    :param args: list of gateways, to be sanitized. +    :type args: list +    """ +    default_device = get_default_device() +    local_network_ipv4 = get_local_network_ipv4(default_device) +    local_network_ipv6 = get_local_network_ipv6(default_device) +    gateways = get_gateways(args) + +    # add custom chain "bitmask" +    if not ipv4_chain_exists(BITMASK_CHAIN): +        ip4tables("--new-chain", BITMASK_CHAIN) +    if not ipv6_chain_exists(BITMASK_CHAIN): +        ip6tables("--new-chain", BITMASK_CHAIN) +    iptables("--insert", "OUTPUT", "--jump", BITMASK_CHAIN) + +    # reject everything +    iptables("--insert", BITMASK_CHAIN, "-o", default_device, +             "--jump", "REJECT") + +    # allow traffic to gateways +    for gateway in gateways: +        ip4tables("--insert", BITMASK_CHAIN, "--destination", gateway, +                  "-o", default_device, "--jump", "ACCEPT") + +    # allow traffic to IPs on local network +    if local_network_ipv4: +        ip4tables("--insert", BITMASK_CHAIN, +                  "--destination", local_network_ipv4, "-o", default_device, +                  "--jump", "ACCEPT") +    if local_network_ipv6: +        ip6tables("--insert", BITMASK_CHAIN, +                  "--destination", local_network_ipv6, "-o", default_device, +                  "--jump", "ACCEPT") + +    # block DNS requests to anyone but the service provider or localhost +    # when we actually route ipv6, we will need dns rules for it too +    ip4tables("--insert", BITMASK_CHAIN, "--protocol", "udp", "--dport", "53", +              "--jump", "REJECT") + +    for allowed_dns in [NAMESERVER, "127.0.0.1", "127.0.1.1"]: +        ip4tables("--insert", BITMASK_CHAIN, "--protocol", "udp", +                  "--dport", "53", "--destination", allowed_dns, +                  "--jump", "ACCEPT") + + +def firewall_stop(): +    """ +    Stop the firewall. +    """ +    iptables("--delete", "OUTPUT", "--jump", BITMASK_CHAIN) +    if ipv4_chain_exists(BITMASK_CHAIN): +        ip4tables("--flush", BITMASK_CHAIN) +        ip4tables("--delete-chain", BITMASK_CHAIN) +    if ipv6_chain_exists(BITMASK_CHAIN): +        ip6tables("--flush", BITMASK_CHAIN) +        ip6tables("--delete-chain", BITMASK_CHAIN) + +## +## MAIN +## + + +def main(): +    if len(sys.argv) >= 3: +        command = "_".join(sys.argv[1:3]) +        args = sys.argv[3:] + +        if command == "openvpn_start": +            openvpn_start(args) + +        elif command == "openvpn_stop": +            openvpn_stop(args) + +        elif command == "firewall_start": +            try: +                firewall_start(args) +                nameserver_setter.start(NAMESERVER) +            except Exception as ex: +                nameserver_restorer.start() +                firewall_stop() +                bail("ERROR: could not start firewall", ex) + +        elif command == "firewall_stop": +            try: +                firewall_stop() +                nameserver_restorer.start() +            except Exception as ex: +                bail("ERROR: could not stop firewall", ex) + +        elif command == "firewall_isup": +            if ipv4_chain_exists(BITMASK_CHAIN): +                print("%s: INFO: bitmask firewall is up" % (SCRIPT,)) +            else: +                bail("INFO: bitmask firewall is down") + +        else: +            bail("ERROR: No such command") +    else: +        bail("ERROR: No such command") + +if __name__ == "__main__": +    if DEBUG: +        logger.debug(" ".join(sys.argv)) +    main() +    print("%s: done" % (SCRIPT,)) +    exit(0) diff --git a/pkg/linux/polkit/net.openvpn.gui.leap.policy b/pkg/linux/polkit/se.leap.bitmask.policy index 50f991a3..c66f4701 100644 --- a/pkg/linux/polkit/net.openvpn.gui.leap.policy +++ b/pkg/linux/polkit/se.leap.bitmask.policy @@ -7,17 +7,17 @@    <vendor>LEAP Project</vendor>    <vendor_url>http://leap.se/</vendor_url> -  <action id="net.openvpn,gui.leap.run-openvpn"> -    <description>Runs the openvpn binary</description> -    <description xml:lang="es">Ejecuta el binario openvpn</description> -    <message>OpenVPN needs that you authenticate to start</message> -    <message xml:lang="es">OpenVPN necesita autorizacion para comenzar</message> +  <action id="se.leap.bitmask.policy"> +    <description>Runs bitmask helper to launch firewall and openvpn</description> +    <description xml:lang="es">Ejecuta el asistente de bitmask para lanzar el firewall y openvpn</description> +    <message>Bitmask needs that you authenticate to start</message> +    <message xml:lang="es">Bitmask necesita autorizacion para comenzar</message>      <icon_name>package-x-generic</icon_name>       <defaults>        <allow_any>yes</allow_any>        <allow_inactive>yes</allow_inactive>        <allow_active>yes</allow_active>      </defaults> -    <annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/openvpn</annotate> +    <annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/bitmask-root</annotate>    </action>  </policyconfig> diff --git a/pkg/linux/update-resolv-conf b/pkg/linux/update-resolv-conf new file mode 100755 index 00000000..76c69413 --- /dev/null +++ b/pkg/linux/update-resolv-conf @@ -0,0 +1,58 @@ +#!/bin/bash +#  +# Parses DHCP options from openvpn to update resolv.conf +# To use set as 'up' and 'down' script in your openvpn *.conf: +# up /etc/leap/update-resolv-conf +# down /etc/leap/update-resolv-conf +# +# Used snippets of resolvconf script by Thomas Hood and Chris Hanson. +# Licensed under the GNU GPL.  See /usr/share/common-licenses/GPL.  +#  +# Example envs set from openvpn: +# +#     foreign_option_1='dhcp-option DNS 193.43.27.132' +#     foreign_option_2='dhcp-option DNS 193.43.27.133' +#     foreign_option_3='dhcp-option DOMAIN be.bnc.ch' +# + +[ -x /sbin/resolvconf ] || exit 0 +[ "$script_type" ] || exit 0 +[ "$dev" ] || exit 0 + +split_into_parts() +{ +	part1="$1" +	part2="$2" +	part3="$3" +} + +case "$script_type" in +  up) +	NMSRVRS="" +	SRCHS="" +	for optionvarname in ${!foreign_option_*} ; do +		option="${!optionvarname}" +		echo "$option" +		split_into_parts $option +		if [ "$part1" = "dhcp-option" ] ; then +			if [ "$part2" = "DNS" ] ; then +				NMSRVRS="${NMSRVRS:+$NMSRVRS }$part3" +			elif [ "$part2" = "DOMAIN" ] ; then +				SRCHS="${SRCHS:+$SRCHS }$part3" +			fi +		fi +	done +	R="" +	[ "$SRCHS" ] && R="search $SRCHS +" +	for NS in $NMSRVRS ; do +        	R="${R}nameserver $NS +" +	done +	echo -n "$R" | /sbin/resolvconf -a "${dev}.openvpn" +	;; +  down) +	/sbin/resolvconf -d "${dev}.openvpn" +	;; +esac + diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py index 8dd9f799..a2df465d 100644 --- a/src/leap/bitmask/backend.py +++ b/src/leap/bitmask/backend.py @@ -17,12 +17,15 @@  """  Backend for everything  """ +import commands  import logging  import os +import time  from functools import partial  from Queue import Queue, Empty +from twisted.internet import reactor  from twisted.internet import threads, defer  from twisted.internet.task import LoopingCall  from twisted.python import log @@ -32,6 +35,7 @@ import zope.interface  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.srpauth import SRPAuth  from leap.bitmask.crypto.srpregister import SRPRegister +from leap.bitmask.platform_init import IS_LINUX  from leap.bitmask.provider import get_provider_path  from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper  from leap.bitmask.services.eip import eipconfig @@ -374,6 +378,34 @@ class EIP(object):          Stop the service.          """          self._vpn.terminate(shutdown) +        if IS_LINUX: +            self._wait_for_firewall_down() + +    def _wait_for_firewall_down(self): +        """ +        Wait for the firewall to come down. +        """ +        # Due to how we delay the resolvconf action in linux. +        # XXX this *has* to wait for a reasonable lapse, since we have some +        # delay in vpn.terminate. +        # For a better solution it should be signaled from backend that +        # everything is clear to proceed, or a timeout happened. +        MAX_FW_WAIT_RETRIES = 25 +        FW_WAIT_STEP = 0.5 + +        retry = 0 + +        fw_up_cmd = "pkexec /usr/sbin/bitmask-root firewall isup" +        fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256 + +        while retry < MAX_FW_WAIT_RETRIES: +            if fw_is_down(): +                return +            else: +                time.sleep(FW_WAIT_STEP) +                retry += 1 +        logger.warning("After waiting, firewall is not down... " +                       "You might experience lack of connectivity")      def terminate(self):          """ @@ -927,9 +959,17 @@ class Backend(object):          """          Stops the looping call and tries to cancel all the defers.          """ +        reactor.callLater(2, self._stop) + +    def _stop(self): +        """ +        Delayed stopping of worker. Called from `stop`. +        """          log.msg("Stopping worker...")          if self._lc.running:              self._lc.stop() +        else: +            logger.warning("Looping call is not running, cannot stop")          while len(self._ongoing_defers) > 0:              d = self._ongoing_defers.pop()              d.cancel() @@ -1111,6 +1151,9 @@ class Backend(object):      def stop_eip(self, shutdown=False):          """          Stop the EIP service. + +        :param shutdown: +        :type shutdown: bool          """          self._call_queue.put(("eip", "stop", None, shutdown)) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 6e270de1..e3848c46 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -19,6 +19,7 @@ Main window for Bitmask.  """  import logging  import socket +import time  from threading import Condition  from datetime import datetime @@ -2109,12 +2110,25 @@ class MainWindow(QtGui.QMainWindow):          logger.debug('Terminating vpn')          self._backend.stop_eip(shutdown=True) +        # We need to give some time to the ongoing signals for shutdown +        # to come into action. This needs to be solved using +        # back-communication from backend. +        QtCore.QTimer.singleShot(3000, self._shutdown) + +    def _shutdown(self): +        """ +        Actually shutdown. +        """          self._cancel_ongoing_defers()          # TODO missing any more cancels?          logger.debug('Cleaning pidfiles')          self._cleanup_pidfiles() +        if self._quit_callback: +            self._quit_callback() + +        logger.debug('Bye.')      def quit(self):          """ @@ -2150,8 +2164,3 @@ class MainWindow(QtGui.QMainWindow):              self._logger_window.close()          self.close() - -        if self._quit_callback: -            self._quit_callback() - -        logger.debug('Bye.') diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py index f39d0bbe..dfd69033 100644 --- a/src/leap/bitmask/gui/twisted_main.py +++ b/src/leap/bitmask/gui/twisted_main.py @@ -26,7 +26,6 @@ logger = logging.getLogger(__name__)  def stop(): -    logger.debug("Really stoping all the things...")      QtCore.QCoreApplication.sendPostedEvents()      QtCore.QCoreApplication.flush()      try: diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index d93efbc6..f2710c58 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -366,15 +366,8 @@ def _linux_install_missing_scripts(badexec, notfound):          fd, tempscript = tempfile.mkstemp(prefix="leap_installer-")          polfd, pol_tempfile = tempfile.mkstemp(prefix="leap_installer-")          try: -            path = launcher.OPENVPN_BIN_PATH -            policy_contents = privilege_policies.get_policy_contents(path) - -            with os.fdopen(polfd, 'w') as f: -                f.write(policy_contents) -              pkexec = first(launcher.maybe_pkexec()) -            scriptlines = launcher.cmd_for_missing_scripts(installer_path, -                                                           pol_tempfile) +            scriptlines = launcher.cmd_for_missing_scripts(installer_path)              with os.fdopen(fd, 'w') as f:                  f.write(scriptlines) diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py index 8747daa6..1f0813e0 100644 --- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -25,7 +25,6 @@ import sys  import time  from leap.bitmask.config import flags -from leap.bitmask.util import privilege_policies  from leap.bitmask.util.privilege_policies import LinuxPolicyChecker  from leap.common.files import which  from leap.bitmask.services.eip.vpnlauncher import VPNLauncher @@ -36,6 +35,8 @@ from leap.bitmask.util import first  logger = logging.getLogger(__name__) +COM = commands +  class EIPNoPolkitAuthAgentAvailable(VPNLauncherException):      pass @@ -64,10 +65,10 @@ def _is_auth_agent_running():      """      # the [x] thing is to avoid grep match itself      polkit_options = [ -        'ps aux | grep polkit-[g]nome-authentication-agent-1', -        'ps aux | grep polkit-[k]de-authentication-agent-1', -        'ps aux | grep polkit-[m]ate-authentication-agent-1', -        'ps aux | grep [l]xpolkit' +        'ps aux | grep "polkit-[g]nome-authentication-agent-1"', +        'ps aux | grep "polkit-[k]de-authentication-agent-1"', +        'ps aux | grep "polkit-[m]ate-authentication-agent-1"', +        'ps aux | grep "[l]xpolkit"'      ]      is_running = [commands.getoutput(cmd) for cmd in polkit_options]      return any(is_running) @@ -85,35 +86,39 @@ def _try_to_launch_agent():          # will do "sh -c 'foo'", so if we do not quoute it we'll end          # up with a invocation to the python interpreter. And that          # is bad. +        logger.debug("Trying to launch polkit agent")          subprocess.call(["python -m leap.bitmask.util.polkit_agent"],                          shell=True, env=env)      except Exception as exc:          logger.exception(exc) +SYSTEM_CONFIG = "/etc/leap" +leapfile = lambda f: "%s/%s" % (SYSTEM_CONFIG, f) + +  class LinuxVPNLauncher(VPNLauncher):      PKEXEC_BIN = 'pkexec' -    OPENVPN_BIN = 'openvpn' -    OPENVPN_BIN_PATH = os.path.join( -        get_path_prefix(), "..", "apps", "eip", OPENVPN_BIN) - -    SYSTEM_CONFIG = "/etc/leap" -    UP_DOWN_FILE = "resolv-update" -    UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE) +    BITMASK_ROOT = "/usr/sbin/bitmask-root"      # We assume this is there by our openvpn dependency, and      # we will put it there on the bundle too. -    # TODO adapt to the bundle path. -    OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/" -    OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so" -    OPENVPN_DOWN_ROOT_PATH = "%s/%s" % ( -        OPENVPN_DOWN_ROOT_BASE, -        OPENVPN_DOWN_ROOT_FILE) - -    UP_SCRIPT = DOWN_SCRIPT = UP_DOWN_PATH -    UPDOWN_FILES = (UP_DOWN_PATH,) +    if flags.STANDALONE: +        OPENVPN_BIN_PATH = "/usr/sbin/leap-openvpn" +    else: +        OPENVPN_BIN_PATH = "/usr/sbin/openvpn" +      POLKIT_PATH = LinuxPolicyChecker.get_polkit_path() -    OTHER_FILES = (POLKIT_PATH, ) + +    if flags.STANDALONE: +        RESOLVCONF_BIN_PATH = "/usr/local/sbin/leap-resolvconf" +    else: +        # this only will work with debian/ubuntu distros. +        RESOLVCONF_BIN_PATH = "/sbin/resolvconf" + +    # XXX openvpn binary TOO +    OTHER_FILES = (POLKIT_PATH, BITMASK_ROOT, OPENVPN_BIN_PATH, +                   RESOLVCONF_BIN_PATH)      @classmethod      def maybe_pkexec(kls): @@ -131,7 +136,7 @@ class LinuxVPNLauncher(VPNLauncher):          if _is_pkexec_in_system():              if not _is_auth_agent_running():                  _try_to_launch_agent() -                time.sleep(0.5) +                time.sleep(2)              if _is_auth_agent_running():                  pkexec_possibilities = which(kls.PKEXEC_BIN)                  leap_assert(len(pkexec_possibilities) > 0, @@ -146,28 +151,6 @@ class LinuxVPNLauncher(VPNLauncher):              raise EIPNoPkexecAvailable()      @classmethod -    def missing_other_files(kls): -        """ -        'Extend' the VPNLauncher's missing_other_files to check if the polkit -        files is outdated, in the case of an standalone bundle. -        If the polkit file that is in OTHER_FILES exists but is not up to date, -        it is added to the missing list. - -        :returns: a list of missing files -        :rtype: list of str -        """ -        # we use `super` in order to send the class to use -        missing = super(LinuxVPNLauncher, kls).missing_other_files() - -        if flags.STANDALONE: -            polkit_file = LinuxPolicyChecker.get_polkit_path() -            if polkit_file not in missing: -                if privilege_policies.is_policy_outdated(kls.OPENVPN_BIN_PATH): -                    missing.append(polkit_file) - -        return missing - -    @classmethod      def get_vpn_command(kls, eipconfig, providerconfig, socket_host,                          socket_port="unix", openvpn_verb=1):          """ @@ -198,6 +181,10 @@ class LinuxVPNLauncher(VPNLauncher):          command = super(LinuxVPNLauncher, kls).get_vpn_command(              eipconfig, providerconfig, socket_host, socket_port, openvpn_verb) +        command.insert(0, kls.BITMASK_ROOT) +        command.insert(1, "openvpn") +        command.insert(2, "start") +          pkexec = kls.maybe_pkexec()          if pkexec:              command.insert(0, first(pkexec)) @@ -205,26 +192,44 @@ class LinuxVPNLauncher(VPNLauncher):          return command      @classmethod -    def cmd_for_missing_scripts(kls, frompath, pol_file): +    def cmd_for_missing_scripts(kls, frompath):          """          Returns a sh script that can copy the missing files. -        :param frompath: The path where the up/down scripts live +        :param frompath: The path where the helper files live          :type frompath: str -        :param pol_file: The path where the dynamically generated -                         policy file lives -        :type pol_file: str          :rtype: str          """ -        to = kls.SYSTEM_CONFIG +        # no system config for now +        # sys_config = kls.SYSTEM_CONFIG +        (polkit_file, openvpn_bin_file, +         bitmask_root_file, resolvconf_bin_file) = map( +            lambda p: os.path.split(p)[-1], +            (kls.POLKIT_PATH, kls.OPENVPN_BIN_PATH, +             kls.BITMASK_ROOT, kls.RESOLVCONF_BIN_PATH))          cmd = '#!/bin/sh\n' -        cmd += 'mkdir -p "%s"\n' % (to, ) -        cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to) -        cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH) +        cmd += 'mkdir -p /usr/local/sbin\n' + +        cmd += 'cp "%s" "%s"\n' % (os.path.join(frompath, polkit_file), +                                   kls.POLKIT_PATH)          cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, ) +        cmd += 'cp "%s" "%s"\n' % (os.path.join(frompath, bitmask_root_file), +                                   kls.BITMASK_ROOT) +        cmd += 'chmod 744 "%s"\n' % (kls.BITMASK_ROOT, ) + +        if flags.STANDALONE: +            cmd += 'cp "%s" "%s"\n' % ( +                os.path.join(frompath, openvpn_bin_file), +                kls.OPENVPN_BIN_PATH) +            cmd += 'chmod 744 "%s"\n' % (kls.POLKIT_PATH, ) + +            cmd += 'cp "%s" "%s"\n' % ( +                os.path.join(frompath, resolvconf_bin_file), +                kls.RESOLVCONF_BIN_PATH) +            cmd += 'chmod 744 "%s"\n' % (kls.POLKIT_PATH, )          return cmd      @classmethod diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index 99cae7f9..dcb48e8a 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -25,14 +25,12 @@ 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.platform_init import IS_LINUX  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__) @@ -107,10 +105,43 @@ class VPNLauncher(object):      @classmethod      @abstractmethod +    def get_gateways(kls, eipconfig, providerconfig): +        """ +        Return the selected gateways for a given provider, looking at the EIP +        config file. + +        :param eipconfig: eip configuration object +        :type eipconfig: EIPConfig + +        :param providerconfig: provider specific configuration +        :type providerconfig: ProviderConfig + +        :rtype: list +        """ +        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('No gateway was found!') + +        logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) +        return gateways + +    @classmethod +    @abstractmethod      def get_vpn_command(kls, eipconfig, providerconfig,                          socket_host, socket_port, openvpn_verb=1):          """ -        Returns the platform dependant vpn launching command +        Return the platform-dependant vpn command for launching openvpn.          Might raise:              OpenVPNNotFoundException, @@ -134,16 +165,19 @@ class VPNLauncher(object):          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: +        # XXX this still has to be changed on osx and windows accordingly +        #kwargs = {} +        #openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) +        #if not openvpn_possibilities: +            #raise OpenVPNNotFoundException() +        #openvpn = first(openvpn_possibilities) +        # ----------------------------------------- +        if not os.path.isfile(kls.OPENVPN_BIN_PATH): +            logger.warning("Could not find openvpn bin in path %s" % ( +                kls.OPENVPN_BIN_PATH))              raise OpenVPNNotFoundException() -        openvpn = first(openvpn_possibilities) +        openvpn = kls.OPENVPN_BIN_PATH          args = []          args += [ @@ -154,22 +188,7 @@ class VPNLauncher(object):          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('No gateway was found!') - -        logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) +        gateways = kls.get_gateways(eipconfig, providerconfig)          for gw in gateways:              args += ['--remote', gw, '1194', 'udp'] @@ -177,11 +196,6 @@ class VPNLauncher(object):          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', @@ -194,15 +208,6 @@ class VPNLauncher(object):          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 @@ -226,20 +231,6 @@ class VPNLauncher(object):                      '--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), @@ -271,13 +262,18 @@ class VPNLauncher(object):          :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] +        # FIXME +        # XXX remove method when we ditch UPDOWN in osx and win too +        if IS_LINUX: +            return [] +        else: +            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): diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index c7b8071c..1559ea8b 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -21,6 +21,7 @@ import logging  import os  import shutil  import socket +import subprocess  import sys  from itertools import chain, repeat @@ -36,10 +37,11 @@ except ImportError:  from leap.bitmask.config import flags  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.services.eip import get_vpn_launcher +from leap.bitmask.services.eip import linuxvpnlauncher  from leap.bitmask.services.eip.eipconfig import EIPConfig  from leap.bitmask.services.eip.udstelnet import UDSTelnet  from leap.bitmask.util import first -from leap.bitmask.platform_init import IS_MAC +from leap.bitmask.platform_init import IS_MAC, IS_LINUX  from leap.common.check import leap_assert, leap_assert_type  logger = logging.getLogger(__name__) @@ -66,9 +68,8 @@ class VPNObserver(object):              'Network is unreachable (code=101)',),          'PROCESS_RESTART_TLS': (              "SIGUSR1[soft,tls-error]",), -        # Let ping-restart work as it should -        # 'PROCESS_RESTART_PING': ( -        #     "SIGUSR1[soft,ping-restart]",), +        'PROCESS_RESTART_PING': ( +            "SIGTERM[soft,ping-restart]",),          'INITIALIZATION_COMPLETED': (              "Initialization Sequence Completed",),      } @@ -159,6 +160,8 @@ class VPN(object):          self._signaler = kwargs['signaler']          self._openvpn_verb = flags.OPENVPN_VERBOSITY +        self._user_stopped = False +      def start(self, *args, **kwargs):          """          Starts the openvpn subprocess. @@ -170,6 +173,7 @@ class VPN(object):          :type kwargs: dict          """          logger.debug('VPN: start') +        self._user_stopped = False          self._stop_pollers()          kwargs['openvpn_verb'] = self._openvpn_verb          kwargs['signaler'] = self._signaler @@ -181,6 +185,15 @@ class VPN(object):              logger.info("Another vpn process is running. Will try to stop it.")              vpnproc.stop_if_already_running() +        # we try to bring the firewall up +        if IS_LINUX: +            gateways = vpnproc.getGateways() +            firewall_up = self._launch_firewall(gateways) +            if not firewall_up: +                logger.error("Could not bring firewall up, " +                             "aborting openvpn launch.") +                return +          cmd = vpnproc.getCommand()          env = os.environ          for key, val in vpnproc.vpn_env.items(): @@ -198,9 +211,37 @@ class VPN(object):          self._pollers.extend(poll_list)          self._start_pollers() +    def _launch_firewall(self, gateways): +        """ +        Launch the firewall using the privileged wrapper. + +        :param gateways: +        :type gateways: list + +        :returns: True if the exitcode of calling the root helper in a +                  subprocess is 0. +        :rtype: bool +        """ +        # XXX could check for wrapper existence, check it's root owned etc. +        # XXX could check that the iptables rules are in place. + +        BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT +        exitCode = subprocess.call(["pkexec", +                                    BM_ROOT, "firewall", "start"] + gateways) +        return True if exitCode is 0 else False + +    def _tear_down_firewall(self): +        """ +        Tear the firewall down using the privileged wrapper. +        """ +        BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT +        exitCode = subprocess.call(["pkexec", +                                    BM_ROOT, "firewall", "stop"]) +        return True if exitCode is 0 else False +      def _kill_if_left_alive(self, tries=0):          """ -        Check if the process is still alive, and sends a +        Check if the process is still alive, and send a          SIGKILL after a timeout period.          :param tries: counter of tries, used in recursion @@ -210,6 +251,15 @@ class VPN(object):          while tries < self.TERMINATE_MAXTRIES:              if self._vpnproc.transport.pid is None:                  logger.debug("Process has been happily terminated.") + +                # we try to tear the firewall down +                if IS_LINUX and self._user_stopped: +                    firewall_down = self._tear_down_firewall() +                    if firewall_down: +                        logger.debug("Firewall down") +                    else: +                        logger.warning("Could not tear firewall down") +                  return              else:                  logger.debug("Process did not die, waiting...") @@ -246,6 +296,10 @@ class VPN(object):          from twisted.internet import reactor          self._stop_pollers() +        # We assume that the only valid shutodowns are initiated +        # by an user action. +        self._user_stopped = shutdown +          # First we try to be polite and send a SIGTERM...          if self._vpnproc:              self._sentterm = True @@ -253,12 +307,17 @@ class VPN(object):              # ...but we also trigger a countdown to be unpolite              # if strictly needed. - -            # XXX Watch out! This will fail NOW since we are running -            # openvpn as root as a workaround for some connection issues.              reactor.callLater(                  self.TERMINATE_WAIT, self._kill_if_left_alive) +            if shutdown: +                if IS_LINUX and self._user_stopped: +                    firewall_down = self._tear_down_firewall() +                    if firewall_down: +                        logger.debug("Firewall down") +                    else: +                        logger.warning("Could not tear firewall down") +      def _start_pollers(self):          """          Iterate through the registered observers @@ -830,9 +889,20 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):              if not isinstance(c, str):                  command[i] = c.encode(encoding) -        logger.debug("Running VPN with command: {0}".format(command)) +        logger.debug("Running VPN with command: ") +        logger.debug("{0}".format(" ".join(command)))          return command +    def getGateways(self): +        """ +        Get the gateways from the appropiate launcher. + +        :rtype: list +        """ +        gateways = self._launcher.get_gateways( +            self._eipconfig, self._providerconfig) +        return gateways +      # shutdown      def killProcess(self): diff --git a/src/leap/bitmask/util/privilege_policies.py b/src/leap/bitmask/util/privilege_policies.py index 72442553..9d1e2c9a 100644 --- a/src/leap/bitmask/util/privilege_policies.py +++ b/src/leap/bitmask/util/privilege_policies.py @@ -27,35 +27,6 @@ from abc import ABCMeta, abstractmethod  logger = logging.getLogger(__name__) -POLICY_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE policyconfig PUBLIC - "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" - "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> -<policyconfig> - -  <vendor>LEAP Project</vendor> -  <vendor_url>https://leap.se/</vendor_url> - -  <action id="net.openvpn.gui.leap.run-openvpn"> -    <description>Runs the openvpn binary</description> -    <description xml:lang="es">Ejecuta el binario openvpn</description> -    <message>OpenVPN needs that you authenticate to start</message> -    <message xml:lang="es"> -      OpenVPN necesita autorizacion para comenzar -    </message> -    <icon_name>package-x-generic</icon_name> -    <defaults> -      <allow_any>yes</allow_any> -      <allow_inactive>yes</allow_inactive> -      <allow_active>yes</allow_active> -    </defaults> -    <annotate key="org.freedesktop.policykit.exec.path">{path}</annotate> -    <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate> -  </action> -</policyconfig> -""" - -  def is_missing_policy_permissions():      """      Returns True if we do not have implemented a policy checker for this @@ -76,36 +47,6 @@ def is_missing_policy_permissions():      return policy_checker().is_missing_policy_permissions() -def get_policy_contents(openvpn_path): -    """ -    Returns the contents that the policy file should have. - -    :param openvpn_path: the openvpn path to use in the polkit file -    :type openvpn_path: str -    :rtype: str -    """ -    return POLICY_TEMPLATE.format(path=openvpn_path) - - -def is_policy_outdated(path): -    """ -    Returns if the existing polkit file is outdated, comparing if the path -    is correct. - -    :param path: the path that should have the polkit file. -    :type path: str. -    :rtype: bool -    """ -    _system = platform.system() -    platform_checker = _system + "PolicyChecker" -    policy_checker = globals().get(platform_checker, None) -    if policy_checker is None: -        logger.debug("we could not find a policy checker implementation " -                     "for %s" % (_system,)) -        return False -    return policy_checker().is_outdated(path) - -  class PolicyChecker:      """      Abstract PolicyChecker class @@ -129,7 +70,7 @@ class LinuxPolicyChecker(PolicyChecker):      PolicyChecker for Linux      """      LINUX_POLKIT_FILE = ("/usr/share/polkit-1/actions/" -                         "net.openvpn.gui.leap.policy") +                         "se.leap.bitmask.policy")      @classmethod      def get_polkit_path(self): @@ -141,6 +82,8 @@ class LinuxPolicyChecker(PolicyChecker):          return self.LINUX_POLKIT_FILE      def is_missing_policy_permissions(self): +    # FIXME this name is quite confusing, it does not have anything to do with +    # file permissions.          """          Returns True if we could not find the appropriate policykit file          in place @@ -148,22 +91,3 @@ class LinuxPolicyChecker(PolicyChecker):          :rtype: bool          """          return not os.path.isfile(self.LINUX_POLKIT_FILE) - -    def is_outdated(self, path): -        """ -        Returns if the existing polkit file is outdated, comparing if the path -        is correct. - -        :param path: the path that should have the polkit file. -        :type path: str. -        :rtype: bool -        """ -        polkit = None -        try: -            with open(self.LINUX_POLKIT_FILE) as f: -                polkit = f.read() -        except IOError, e: -            logger.error("Error reading polkit file(%s): %r" % ( -                self.LINUX_POLKIT_FILE, e)) - -        return get_policy_contents(path) != polkit  | 
